diff --git a/.github/workflows/free-tests.yml b/.github/workflows/free-tests.yml index 516978e13..19a444876 100644 --- a/.github/workflows/free-tests.yml +++ b/.github/workflows/free-tests.yml @@ -125,10 +125,13 @@ jobs: git config --global user.email "free-tests-ci@gstack.test" git config --global user.name "Free Tests CI" git config --global init.defaultBranch main - - name: Codex probe selects a model when sourced from zsh (macOS default shell, no paid call) + - name: Codex probe selects a model from zsh, run as a command and sourced (macOS default shell, no paid call) working-directory: ${{ runner.temp }} run: | set -euo pipefail + run="$(zsh -c '"$GITHUB_WORKSPACE/bin/gstack-codex-probe" select-model exec' 2>&1)" + printf '%s\n' "$run" + printf '%s\n' "$run" | grep -E '^CODEX_SEL: [A-Za-z0-9._:/-]+$' out="$(zsh -c 'source "$GITHUB_WORKSPACE/bin/gstack-codex-probe" && _gstack_codex_select_model exec' 2>&1)" printf '%s\n' "$out" printf '%s\n' "$out" | grep -E '^CODEX_MODEL: [A-Za-z0-9._:/-]+ \(exec; source: ' @@ -139,7 +142,7 @@ jobs: resolved="$(realpath "$tmp")" echo "temp root: $tmp -> $resolved" test "$resolved" != "${tmp%/}" - files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts) + files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts) for file in "${files[@]}"; do test -f "$file"; done bun test "${files[@]}" diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 00ea5148a..c1796a1bd 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -754,26 +754,30 @@ Review skills will load at each phase entry. Starting full review pipeline with ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -782,12 +786,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -795,7 +799,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." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Keep the required Claude adversarial pass; do not dispatch a duplicate. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Keep the required Claude adversarial pass; do not dispatch a duplicate. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Keep the required Claude adversarial pass; do not dispatch a duplicate. - **`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. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Keep the required Claude adversarial pass; do not dispatch a duplicate. diff --git a/autoplan/sections/ceo-phase.md b/autoplan/sections/ceo-phase.md index 547f79a9c..8f8e36c2e 100644 --- a/autoplan/sections/ceo-phase.md +++ b/autoplan/sections/ceo-phase.md @@ -111,15 +111,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" if [ "$_OUTSIDE_EXIT" -eq 124 ]; then - _gstack_codex_log_event "codex_timeout" "540" || true - _gstack_codex_log_hang "autoplan" "0" || true + "$_CODEX_PROBE" log-event codex_timeout "540" || true + "$_CODEX_PROBE" log-hang autoplan 0 || true fi cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } _OUTSIDE_RC=0 diff --git a/autoplan/sections/design-phase.md b/autoplan/sections/design-phase.md index 011160940..5b72890ed 100644 --- a/autoplan/sections/design-phase.md +++ b/autoplan/sections/design-phase.md @@ -70,15 +70,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" if [ "$_OUTSIDE_EXIT" -eq 124 ]; then - _gstack_codex_log_event "codex_timeout" "540" || true - _gstack_codex_log_hang "autoplan" "0" || true + "$_CODEX_PROBE" log-event codex_timeout "540" || true + "$_CODEX_PROBE" log-hang autoplan 0 || true fi cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } _OUTSIDE_RC=0 diff --git a/autoplan/sections/dx-phase.md b/autoplan/sections/dx-phase.md index dc6074b12..63c1a4fe0 100644 --- a/autoplan/sections/dx-phase.md +++ b/autoplan/sections/dx-phase.md @@ -74,15 +74,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" if [ "$_OUTSIDE_EXIT" -eq 124 ]; then - _gstack_codex_log_event "codex_timeout" "540" || true - _gstack_codex_log_hang "autoplan" "0" || true + "$_CODEX_PROBE" log-event codex_timeout "540" || true + "$_CODEX_PROBE" log-hang autoplan 0 || true fi cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } _OUTSIDE_RC=0 diff --git a/autoplan/sections/eng-phase.md b/autoplan/sections/eng-phase.md index 26cc37953..76cba4ea9 100644 --- a/autoplan/sections/eng-phase.md +++ b/autoplan/sections/eng-phase.md @@ -63,15 +63,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" if [ "$_OUTSIDE_EXIT" -eq 124 ]; then - _gstack_codex_log_event "codex_timeout" "540" || true - _gstack_codex_log_hang "autoplan" "0" || true + "$_CODEX_PROBE" log-event codex_timeout "540" || true + "$_CODEX_PROBE" log-hang autoplan 0 || true fi cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } _OUTSIDE_RC=0 diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index d0b8a72e8..f52f10952 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -1,6 +1,26 @@ #!/usr/bin/env bash -# gstack-codex-probe: shared helper for /codex and /autoplan skills. -# Sourced from template bash blocks; never execute directly. +# gstack-codex-probe: shared helper for /codex, /autoplan and every outside +# review. Run it as a command, one subcommand per probe (`gstack-codex-probe +# help`): it prints KEY: value lines on stdout, human status lines where they +# went before, and sets nothing in the caller's shell. +# +# Subcommands (each wraps one function below): +# select-model [--model ] CODEX_SEL:, CODEX_SEL_KIND:, CODEX_SANDBOX: +# check-auth exit 0 authed, 1 not +# show-sandbox CODEX_SANDBOX: +# check-sandbox exit 0, or 3 with the reason on stderr +# probe-model [--model ] CODEX_PROBE_STATE:; exit 0, 1, 2 or 4 +# check-version warnings on stderr; exit 0 +# show-first-use-notice notice on stderr; exit 0 +# run-with-timeout the command's exit code; 124 on timeout +# log-event [detail] silent; exit 0 +# log-hang silent; exit 0 +# help usage; an unknown subcommand exits 64 +# +# Sourcing (compatibility window): skills rendered before the executed form +# source this file and call the functions directly. That still works, prints +# one deprecation line on stderr, and is removed in a release on or after +# 2026-10-21 together with the zsh self-locate block below. # # Functions (all prefixed with _gstack_codex_ for namespace hygiene): # _gstack_codex_auth_probe — multi-signal auth check (env + file) @@ -14,14 +34,14 @@ # _gstack_codex_timeout_wrapper — gtimeout -> timeout -> bash-native watchdog # _gstack_codex_log_event — telemetry emission to ~/.gstack/analytics/ # -# Hygiene rules (enforced by test/codex-hardening.test.ts): +# Hygiene rules for the sourced form (enforced by test/codex-hardening.test.ts): # - Never change -e / -u / traps / IFS / PATH in the caller shell. # - All internal vars prefix with _GSTACK_CODEX_. # - All functions prefix with _gstack_codex_. -# - No command execution at source time (only function defs and the shared -# self-location block below, which is parameter expansion only; its -# _gstack_helper* variables are the one exception to the prefix rule and -# are unset once _GSTACK_CODEX_BIN is set). +# - No command execution at source time (only function defs, the shared +# self-location block below, which is parameter expansion only, and the +# deprecation line; its _gstack_helper* variables are the one exception to +# the prefix rule and are unset once _GSTACK_CODEX_BIN is set). # --- Self-location (bash and zsh) ------------------------------------------- # Under zsh BASH_SOURCE is empty, so every "$dir/../scripts/..." path became @@ -532,3 +552,93 @@ _gstack_codex_log_hang() { "$_log_bin" "$(printf '{"skill":"codex","type":"operational","key":"%s","insight":"Codex hit its timeout wrapper during [%s] invocation. Prompt size: %s. Consider splitting prompt or checking network.","confidence":8,"source":"observed","files":["codex/SKILL.md.tmpl","autoplan/SKILL.md.tmpl"]}' "$_key" "$_mode" "$_prompt_size")" \ >/dev/null 2>&1 || true } + +# --- Executed subcommands ---------------------------------------------------- + +_gstack_codex_usage() { + cat <<'EOF' +Usage: gstack-codex-probe [args] + select-model [--model ] print CODEX_SEL:, CODEX_SEL_KIND: and CODEX_SANDBOX:; exit 0 or 1 + check-auth exit 0 when Codex credentials exist, 1 otherwise + show-sandbox print CODEX_SANDBOX: read-only (or danger-full-access) + check-sandbox free sandbox check; exit 0, or 3 with the reason on stderr + probe-model [--model ] paid round trip; print CODEX_PROBE_STATE:; exit 0, 1, 2 or 4 + check-version warn on stderr about known-bad Codex CLI versions; exit 0 + show-first-use-notice once per machine, say where reviews go; exit 0 + run-with-timeout run cmd under a deadline; its exit code, 124 on timeout + log-event [detail] record a telemetry event when telemetry is on; exit 0 + log-hang record a timeout learning; exit 0 + help this text +EOF +} + +# Parses " [--model ]" into _GSTACK_CODEX_ARG_KIND and +# _GSTACK_CODEX_ARG_MODEL; returns 64 on anything else. +_gstack_codex_kind_args() { + _GSTACK_CODEX_ARG_KIND="${1:-}" + _GSTACK_CODEX_ARG_MODEL="" + case "$_GSTACK_CODEX_ARG_KIND" in exec|review) shift ;; *) return 64 ;; esac + if [ "$#" -eq 2 ] && [ "$1" = "--model" ] && [ -n "$2" ]; then + _GSTACK_CODEX_ARG_MODEL="$2" + elif [ "$#" -ne 0 ]; then + return 64 + fi +} + +_gstack_codex_main() { + local _cmd="${1:-}" _rc + [ "$#" -gt 0 ] && shift + case "$_cmd" in + log-event|probe-model|check-sandbox|check-version) + # Telemetry is the caller's choice when it exports _TEL, else gstack-config's. + [ -n "${_TEL+set}" ] || _TEL=$("$_GSTACK_CODEX_BIN/gstack-config" get telemetry 2>/dev/null || echo off) ;; + esac + case "$_cmd" in + help|-h|--help) _gstack_codex_usage ;; + select-model) + _gstack_codex_kind_args "$@" || { _gstack_codex_usage >&2; return 64; } + _gstack_codex_select_model "$_GSTACK_CODEX_ARG_KIND" "$_GSTACK_CODEX_ARG_MODEL" || return 1 + printf 'CODEX_SEL: %s\nCODEX_SEL_KIND: %s\nCODEX_SANDBOX: %s\n' "$_GSTACK_CODEX_SEL" "$_GSTACK_CODEX_SEL_KIND" "$_GSTACK_CODEX_SANDBOX" ;; + check-auth) [ "$#" -eq 0 ] || { _gstack_codex_usage >&2; return 64; }; _gstack_codex_auth_probe ;; + show-sandbox) + [ "$#" -eq 0 ] || { _gstack_codex_usage >&2; return 64; } + _gstack_codex_sandbox_mode + printf 'CODEX_SANDBOX: %s\n' "$_GSTACK_CODEX_SANDBOX" ;; + check-sandbox) [ "$#" -eq 0 ] || { _gstack_codex_usage >&2; return 64; }; _gstack_codex_sandbox_preflight >&2 ;; + probe-model) + _gstack_codex_kind_args "$@" || { _gstack_codex_usage >&2; return 64; } + if [ -n "$_GSTACK_CODEX_ARG_MODEL" ] && ! _gstack_codex_select_model "$_GSTACK_CODEX_ARG_KIND" "$_GSTACK_CODEX_ARG_MODEL"; then + echo "MODEL_UNUSABLE" + echo "HINT: the requested Codex model is not a valid model id; fix it as described above." + echo "CODEX_PROBE_STATE: unusable" + return 1 + fi + _gstack_codex_model_probe "$_GSTACK_CODEX_ARG_KIND" + _rc=$? + case "$_rc" in + 0) echo "CODEX_PROBE_STATE: ${_GSTACK_CODEX_PROBE_STATE:-ok}" ;; + 1) echo "CODEX_PROBE_STATE: unusable" ;; + 2) echo "CODEX_PROBE_STATE: broken_install" ;; + 4) echo "CODEX_PROBE_STATE: quota_exhausted" ;; + *) echo "CODEX_PROBE_STATE: error" ;; + esac + return "$_rc" ;; + check-version) [ "$#" -eq 0 ] || { _gstack_codex_usage >&2; return 64; }; _gstack_codex_version_check >&2; return 0 ;; + show-first-use-notice) [ "$#" -eq 0 ] || { _gstack_codex_usage >&2; return 64; }; _gstack_codex_first_use_notice; return 0 ;; + run-with-timeout) + case "${1:-}" in ''|*[!0-9.]*) _gstack_codex_usage >&2; return 64 ;; esac + [ "$#" -ge 2 ] || { _gstack_codex_usage >&2; return 64; } + _gstack_codex_timeout_wrapper "$@" ;; + log-event) [ "$#" -ge 1 ] && _gstack_codex_log_event "$@" >/dev/null 2>&1; return 0 ;; + log-hang) _gstack_codex_log_hang "$@" >/dev/null 2>&1; return 0 ;; + *) _gstack_codex_usage >&2; return 64 ;; + esac +} + +# Executed (bash through the shebang, $0 names this file): run one subcommand. +# Sourced, including eval and zsh: keep the functions and say the form is going. +if [ -n "${BASH_VERSION:-}" ] && [ "${0##*/}" = "gstack-codex-probe" ]; then + _gstack_codex_main "$@" + exit $? +fi +printf '%s\n' "gstack: sourcing gstack-codex-probe is deprecated and stops working in a release on or after 2026-10-21. Run /gstack-upgrade so your skills run it as a command (gstack-codex-probe help)." >&2 diff --git a/codex/SKILL.md b/codex/SKILL.md index 330c87a0e..2a73e34ef 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -454,8 +454,7 @@ If `NOT_FOUND`: stop and tell the user: If `NOT_FOUND`, also log the event: ```bash -_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off) -source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null && _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +~/.claude/skills/gstack/bin/gstack-codex-probe log-event codex_cli_missing 2>/dev/null || true ``` --- @@ -464,19 +463,19 @@ source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null && _gstack_cod Before building expensive prompts, verify Codex has valid auth, that the account can actually USE gstack's selected model, AND the installed CLI version isn't in the -known-bad list. Sourcing `gstack-codex-probe` loads the shared helpers that both -`/codex` and `/autoplan` use. +known-bad list. `gstack-codex-probe` is the shared helper that `/codex`, `/autoplan` +and every outside review run as a command, one subcommand per check. Model order: a model the user names for this request, `GSTACK_CODEX_MODEL`, Codex `config.toml` `model` (`review_model` first for `codex review`; honors `$CODEX_HOME`), then `gpt-6-astra`. Each call prints `CODEX_MODEL: (; source: ...)` first. -For a named model, pass it as the second argument of every `_gstack_codex_select_model` -call, and run `_gstack_codex_select_model exec ''` before the probe below. An -invalid or unavailable choice stops with a repair message, never the default. +For a named model, add `--model ''` to every `select-model` and `probe-model` +call, including the probe below. An invalid or unavailable choice stops with a repair +message, never the default. ```bash -_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off) -source ~/.claude/skills/gstack/bin/gstack-codex-probe || { echo "HELPER_UNAVAILABLE"; exit 1; } +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe +[ -x "$_CODEX_PROBE" ] || { echo "HELPER_UNAVAILABLE"; exit 1; } # GSTACK_ACTIVE_HOST names the harness, never the model. if { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then @@ -488,20 +487,19 @@ if { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK fi exit 78 fi -if ! _gstack_codex_auth_probe >/dev/null; then - _gstack_codex_log_event "codex_auth_failed" +if ! "$_CODEX_PROBE" check-auth >/dev/null; then + "$_CODEX_PROBE" log-event codex_auth_failed echo "AUTH_FAILED" -elif _gstack_codex_sandbox_preflight; then # free; Linux only - _gstack_codex_model_probe # ~10s round trip on first run, cached 1h +elif "$_CODEX_PROBE" check-sandbox; then # free; Linux only + "$_CODEX_PROBE" probe-model exec # ~10s round trip on first run, cached 1h fi -_gstack_codex_version_check # warns if known-bad, non-blocking +"$_CODEX_PROBE" check-version # warns if known-bad, non-blocking ``` If the runtime guard reports a harness mismatch, stop. Outside coverage is unavailable. Repair with `./setup --host codex`; do not silently substitute another provider or force a same-harness invocation. -If the output contains `HELPER_UNAVAILABLE`, stop: the gstack helper could not load in -this shell. Relay its `gstack: cannot locate ...` line verbatim; it names the shell and -links the fix. +If the output contains `HELPER_UNAVAILABLE`, stop: `gstack-codex-probe` is missing or +not executable in this install. Tell the user to re-run `./setup` (or `/gstack-upgrade`). 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." @@ -773,7 +771,7 @@ table-bearing section that must be the file's terminal heading. ## Model & Reasoning -**Model:** every Codex call passes the selection above via `-c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false`. +**Model:** every Codex call passes the selection above via `-c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false`. Native `codex review` selects with `review` and sets both `model` and `review_model`. The flag also keeps installed skills out of Codex's context, so a review cannot become a nested skill run. @@ -871,7 +869,7 @@ If token count is not available, display: `Tokens: unknown` before showing it. Show it in full inside the CODEX SAYS block. - **Add synthesis after, not instead of.** Any Claude commentary comes after the full output. - **Bash gate above the wrapper.** Every Bash call to codex sets its `timeout` - parameter ABOVE the inner `_gstack_codex_timeout_wrapper` budget (Review: + parameter ABOVE the inner `run-with-timeout` budget (Review: `timeout: 360000` over the 330s wrapper; Challenge/Consult: `timeout: 600000` over the 540s wrappers) so the wrapper fires first with a diagnosable exit 124. - **No double-reviewing.** If the user already ran `/review`, Codex provides a second diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 6e5e2e51d..61e5a440b 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -55,8 +55,7 @@ If `NOT_FOUND`: stop and tell the user: If `NOT_FOUND`, also log the event: ```bash -_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off) -source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null && _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +~/.claude/skills/gstack/bin/gstack-codex-probe log-event codex_cli_missing 2>/dev/null || true ``` --- @@ -65,35 +64,34 @@ source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null && _gstack_cod Before building expensive prompts, verify Codex has valid auth, that the account can actually USE gstack's selected model, AND the installed CLI version isn't in the -known-bad list. Sourcing `gstack-codex-probe` loads the shared helpers that both -`/codex` and `/autoplan` use. +known-bad list. `gstack-codex-probe` is the shared helper that `/codex`, `/autoplan` +and every outside review run as a command, one subcommand per check. Model order: a model the user names for this request, `GSTACK_CODEX_MODEL`, Codex `config.toml` `model` (`review_model` first for `codex review`; honors `$CODEX_HOME`), then `gpt-6-astra`. Each call prints `CODEX_MODEL: (; source: ...)` first. -For a named model, pass it as the second argument of every `_gstack_codex_select_model` -call, and run `_gstack_codex_select_model exec ''` before the probe below. An -invalid or unavailable choice stops with a repair message, never the default. +For a named model, add `--model ''` to every `select-model` and `probe-model` +call, including the probe below. An invalid or unavailable choice stops with a repair +message, never the default. ```bash -_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off) -source ~/.claude/skills/gstack/bin/gstack-codex-probe || { echo "HELPER_UNAVAILABLE"; exit 1; } +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe +[ -x "$_CODEX_PROBE" ] || { echo "HELPER_UNAVAILABLE"; exit 1; } {{OUTSIDE_SELF_GUARD:codex}} -if ! _gstack_codex_auth_probe >/dev/null; then - _gstack_codex_log_event "codex_auth_failed" +if ! "$_CODEX_PROBE" check-auth >/dev/null; then + "$_CODEX_PROBE" log-event codex_auth_failed echo "AUTH_FAILED" -elif _gstack_codex_sandbox_preflight; then # free; Linux only - _gstack_codex_model_probe # ~10s round trip on first run, cached 1h +elif "$_CODEX_PROBE" check-sandbox; then # free; Linux only + "$_CODEX_PROBE" probe-model exec # ~10s round trip on first run, cached 1h fi -_gstack_codex_version_check # warns if known-bad, non-blocking +"$_CODEX_PROBE" check-version # warns if known-bad, non-blocking ``` If the runtime guard reports a harness mismatch, stop. Outside coverage is unavailable. Repair with `./setup --host codex`; do not silently substitute another provider or force a same-harness invocation. -If the output contains `HELPER_UNAVAILABLE`, stop: the gstack helper could not load in -this shell. Relay its `gstack: cannot locate ...` line verbatim; it names the shell and -links the fix. +If the output contains `HELPER_UNAVAILABLE`, stop: `gstack-codex-probe` is missing or +not executable in this install. Tell the user to re-run `./setup` (or `/gstack-upgrade`). 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." @@ -338,7 +336,7 @@ If token count is not available, display: `Tokens: unknown` before showing it. Show it in full inside the CODEX SAYS block. - **Add synthesis after, not instead of.** Any Claude commentary comes after the full output. - **Bash gate above the wrapper.** Every Bash call to codex sets its `timeout` - parameter ABOVE the inner `_gstack_codex_timeout_wrapper` budget (Review: + parameter ABOVE the inner `run-with-timeout` budget (Review: `timeout: 360000` over the 330s wrapper; Challenge/Consult: `timeout: 600000` over the 540s wrappers) so the wrapper fires first with a diagnosable exit 124. - **No double-reviewing.** If the user already ran `/review`, Codex provides a second diff --git a/codex/sections/challenge-mode.md b/codex/sections/challenge-mode.md index be8b96f47..c27062e81 100644 --- a/codex/sections/challenge-mode.md +++ b/codex/sections/challenge-mode.md @@ -48,11 +48,13 @@ fi # capture stderr to $TMPERR for auth error detection (was: 2>/dev/null). [ -n "${TMPERR:-}" ] || TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; } TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; } -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model exec || exit 1 +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') PROMPT_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" [ -s "$PROMPT_FILE" ] || { echo "Not run: $PROMPT_FILE is missing or empty, so the prompt was never written. Write it, then run by hand: codex exec - -C $_REPO_ROOT < $PROMPT_FILE" >&2; exit 1; } -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " import sys, json turn_completed_count = 0 turn_failed = False @@ -94,20 +96,20 @@ elif turn_completed_count == 0: _CODEX_EXIT=${PIPESTATUS[0]:-${pipestatus[1]}} # bash sets PIPESTATUS; zsh (lowercase, 1-indexed) falls through # Fix 1: hang detection — log + surface actionable message if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "540" - _gstack_codex_log_hang "challenge" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "540" + "$_CODEX_PROBE" log-hang "challenge" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 9 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/." elif [ "$_CODEX_EXIT" != "0" ]; then # Surface non-zero exits so the calling agent doesn't read "no output" as # a silent model/API stall. echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")" head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true - _gstack_codex_log_event "codex_nonzero_exit" "challenge:$_CODEX_EXIT" + "$_CODEX_PROBE" log-event codex_nonzero_exit "challenge:$_CODEX_EXIT" fi # Fix 2: surface auth errors from captured stderr instead of dropping them if grep -qiE "auth|login|unauthorized" "$TMPERR" 2>/dev/null; then echo "[codex auth error] $(head -1 "$TMPERR")" - _gstack_codex_log_event "codex_auth_failed" + "$_CODEX_PROBE" log-event codex_auth_failed fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex challenge' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP" rm -f "$TMPRESP" "$TMPRESP.events" "$PROMPT_FILE" diff --git a/codex/sections/challenge-mode.md.tmpl b/codex/sections/challenge-mode.md.tmpl index 00c3afccb..8d9d30dee 100644 --- a/codex/sections/challenge-mode.md.tmpl +++ b/codex/sections/challenge-mode.md.tmpl @@ -39,11 +39,10 @@ fi # capture stderr to $TMPERR for auth error detection (was: 2>/dev/null). [ -n "${TMPERR:-}" ] || TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; } TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; } -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model exec || exit 1 +{{CODEX_SELECT:exec}} PROMPT_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" [ -s "$PROMPT_FILE" ] || { echo "Not run: $PROMPT_FILE is missing or empty, so the prompt was never written. Write it, then run by hand: codex exec - -C $_REPO_ROOT < $PROMPT_FILE" >&2; exit 1; } -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " import sys, json turn_completed_count = 0 turn_failed = False @@ -85,20 +84,20 @@ elif turn_completed_count == 0: _CODEX_EXIT=${PIPESTATUS[0]:-${pipestatus[1]}} # bash sets PIPESTATUS; zsh (lowercase, 1-indexed) falls through # Fix 1: hang detection — log + surface actionable message if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "540" - _gstack_codex_log_hang "challenge" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "540" + "$_CODEX_PROBE" log-hang "challenge" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 9 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/." elif [ "$_CODEX_EXIT" != "0" ]; then # Surface non-zero exits so the calling agent doesn't read "no output" as # a silent model/API stall. echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")" head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true - _gstack_codex_log_event "codex_nonzero_exit" "challenge:$_CODEX_EXIT" + "$_CODEX_PROBE" log-event codex_nonzero_exit "challenge:$_CODEX_EXIT" fi # Fix 2: surface auth errors from captured stderr instead of dropping them if grep -qiE "auth|login|unauthorized" "$TMPERR" 2>/dev/null; then echo "[codex auth error] $(head -1 "$TMPERR")" - _gstack_codex_log_event "codex_auth_failed" + "$_CODEX_PROBE" log-event codex_auth_failed fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex challenge' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP" rm -f "$TMPRESP" "$TMPRESP.events" "$PROMPT_FILE" diff --git a/codex/sections/consult-mode.md b/codex/sections/consult-mode.md index ffeecee96..14f4a6650 100644 --- a/codex/sections/consult-mode.md +++ b/codex/sections/consult-mode.md @@ -101,8 +101,10 @@ fi cd "$_REPO_ROOT" || exit 1 PROMPT_FILE="$_REPO_ROOT/.gstack/tmp/" [ -s "$PROMPT_FILE" ] || { echo "Not run: $PROMPT_FILE is missing or empty, so the prompt was never written. Write it, then run by hand: codex exec - -C $_REPO_ROOT < $PROMPT_FILE" >&2; exit 1; } -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model exec || exit 1 +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') _SID="" if [ "$_CODEX_MODE" = "resume" ]; then _SID=$(cat .context/codex-session-id 2>/dev/null) @@ -111,9 +113,9 @@ fi _LABEL=consult${_SID:+-resume} # Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper) if [ -n "$_SID" ]; then - _gstack_codex_timeout_wrapper 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" else - _gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" fi | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " import sys, json turn_completed_count = 0 @@ -159,15 +161,15 @@ elif turn_completed_count == 0: # Fix 1: hang detection _CODEX_EXIT=${PIPESTATUS[0]:-${pipestatus[1]}} # bash sets PIPESTATUS; zsh (lowercase, 1-indexed) falls through if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "540" - _gstack_codex_log_hang "$_LABEL" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "540" + "$_CODEX_PROBE" log-hang "$_LABEL" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 9 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/." elif [ "$_CODEX_EXIT" != "0" ]; then # Surface non-zero exits so the calling agent doesn't read "no output" as # a silent model/API stall. echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")" head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true - _gstack_codex_log_event "codex_nonzero_exit" "$_LABEL:$_CODEX_EXIT" + "$_CODEX_PROBE" log-event codex_nonzero_exit "$_LABEL:$_CODEX_EXIT" fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex consult' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP" rm -f "$PROMPT_FILE" diff --git a/codex/sections/consult-mode.md.tmpl b/codex/sections/consult-mode.md.tmpl index 076cf413c..81aa5c699 100644 --- a/codex/sections/consult-mode.md.tmpl +++ b/codex/sections/consult-mode.md.tmpl @@ -92,8 +92,7 @@ fi cd "$_REPO_ROOT" || exit 1 PROMPT_FILE="$_REPO_ROOT/.gstack/tmp/" [ -s "$PROMPT_FILE" ] || { echo "Not run: $PROMPT_FILE is missing or empty, so the prompt was never written. Write it, then run by hand: codex exec - -C $_REPO_ROOT < $PROMPT_FILE" >&2; exit 1; } -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model exec || exit 1 +{{CODEX_SELECT:exec}} _SID="" if [ "$_CODEX_MODE" = "resume" ]; then _SID=$(cat .context/codex-session-id 2>/dev/null) @@ -102,9 +101,9 @@ fi _LABEL=consult${_SID:+-resume} # Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper) if [ -n "$_SID" ]; then - _gstack_codex_timeout_wrapper 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" else - _gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" fi | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " import sys, json turn_completed_count = 0 @@ -150,15 +149,15 @@ elif turn_completed_count == 0: # Fix 1: hang detection _CODEX_EXIT=${PIPESTATUS[0]:-${pipestatus[1]}} # bash sets PIPESTATUS; zsh (lowercase, 1-indexed) falls through if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "540" - _gstack_codex_log_hang "$_LABEL" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "540" + "$_CODEX_PROBE" log-hang "$_LABEL" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 9 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/." elif [ "$_CODEX_EXIT" != "0" ]; then # Surface non-zero exits so the calling agent doesn't read "no output" as # a silent model/API stall. echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")" head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true - _gstack_codex_log_event "codex_nonzero_exit" "$_LABEL:$_CODEX_EXIT" + "$_CODEX_PROBE" log-event codex_nonzero_exit "$_LABEL:$_CODEX_EXIT" fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex consult' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP" rm -f "$PROMPT_FILE" diff --git a/codex/sections/review-mode.md b/codex/sections/review-mode.md index 940d914fd..ab4d41f4d 100644 --- a/codex/sections/review-mode.md +++ b/codex/sections/review-mode.md @@ -37,8 +37,8 @@ Use only one command path below. Remember its printed start token as CODEX_REVIE **Sandbox is pinned via config override.** Top-level `codex review` has no `-s`/`--sandbox` flag (verified on 0.147.0: `codex review --help` lists none), so the -sandbox is set with `-c` from `_GSTACK_CODEX_SANDBOX`, which `_gstack_codex_select_model` -sets so the effective setting is `sandbox_mode="read-only"` (full access only for +sandbox is set with `-c` from `_CODEX_SANDBOX_MODE`, which `select-model` reports +so the effective setting is `sandbox_mode="read-only"` (full access only for `GSTACK_CODEX_NO_SANDBOX=1`, with a warning). Without it the call inherits the user's `~/.codex/config.toml` default, which on a trusted project can be WRITE access — contradicting this skill's read-only contract: @@ -47,17 +47,19 @@ contradicting this skill's read-only contract: _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } cd "$_REPO_ROOT" ~/.claude/skills/gstack/bin/gstack-review-log --start codex-review -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model review || exit 1 +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe +_CODEX_OUT=$("$_CODEX_PROBE" select-model review) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') # The 330s wrapper sits BELOW the 360s Bash gate so the wrapper fires FIRST # and a stall surfaces as a diagnosable exit 124 with an explicit message, # never as a silent harness kill that downstream reads as "no findings". -_gstack_codex_timeout_wrapper 330 codex review --base -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" -c "review_model=\"${_GSTACK_CODEX_SEL:?}\"" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null >"$TMPOUT" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout 330 codex review --base -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "review_model=\"${_CODEX_SEL:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null >"$TMPOUT" 2>"$TMPERR" _CODEX_EXIT=$? cat "$TMPOUT" if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "330" - _gstack_codex_log_hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "330" + "$_CODEX_PROBE" log-hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 5.5 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/." elif [ "$_CODEX_EXIT" != "0" ]; then # Surface non-zero exits (parse errors, arg-shape breaks, etc.) so the @@ -65,7 +67,7 @@ elif [ "$_CODEX_EXIT" != "0" ]; then # burn 30-60min misdiagnosing it. echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")" head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true - _gstack_codex_log_event "codex_nonzero_exit" "review:$_CODEX_EXIT" + "$_CODEX_PROBE" log-event codex_nonzero_exit "review:$_CODEX_EXIT" fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex review' --exit "$_CODEX_EXIT" --stderr "$TMPERR" structured "$TMPOUT" ``` @@ -103,8 +105,10 @@ cd "$_REPO_ROOT" FOCUS_FILE="$_REPO_ROOT/.gstack/tmp/" [ -s "$FOCUS_FILE" ] || { echo "Not run: $FOCUS_FILE is missing or empty, so the focus text was never written. Write it, then run this block again." >&2; exit 1; } ~/.claude/skills/gstack/bin/gstack-review-log --start codex-review -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model exec || exit 1 +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; } { printf '%s\n' "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. Do not invoke any installed skill (Codex home skills/, .agents/); answer directly. Do NOT modify agents/openai.yaml. Stay focused on repository code only." @@ -116,13 +120,13 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp git diff "...HEAD" 2>/dev/null printf '\nDIFF_END\n' } > "$_PROMPT_FILE" -_gstack_codex_timeout_wrapper 330 codex exec - -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout 330 codex exec - -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" _CODEX_EXIT=$? rm -f "$_PROMPT_FILE" "$FOCUS_FILE" cat "$TMPOUT" if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "330" - _gstack_codex_log_hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "330" + "$_CODEX_PROBE" log-hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 5.5 minutes." fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex review' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPOUT.events" structured "$TMPOUT" diff --git a/codex/sections/review-mode.md.tmpl b/codex/sections/review-mode.md.tmpl index e452927a1..9a3729d5e 100644 --- a/codex/sections/review-mode.md.tmpl +++ b/codex/sections/review-mode.md.tmpl @@ -35,8 +35,8 @@ Use only one command path below. Remember its printed start token as CODEX_REVIE **Sandbox is pinned via config override.** Top-level `codex review` has no `-s`/`--sandbox` flag (verified on 0.147.0: `codex review --help` lists none), so the -sandbox is set with `-c` from `_GSTACK_CODEX_SANDBOX`, which `_gstack_codex_select_model` -sets so the effective setting is `sandbox_mode="read-only"` (full access only for +sandbox is set with `-c` from `_CODEX_SANDBOX_MODE`, which `select-model` reports +so the effective setting is `sandbox_mode="read-only"` (full access only for `GSTACK_CODEX_NO_SANDBOX=1`, with a warning). Without it the call inherits the user's `~/.codex/config.toml` default, which on a trusted project can be WRITE access — contradicting this skill's read-only contract: @@ -45,17 +45,16 @@ contradicting this skill's read-only contract: _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } cd "$_REPO_ROOT" ~/.claude/skills/gstack/bin/gstack-review-log --start codex-review -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model review || exit 1 +{{CODEX_SELECT:review}} # The 330s wrapper sits BELOW the 360s Bash gate so the wrapper fires FIRST # and a stall surfaces as a diagnosable exit 124 with an explicit message, # never as a silent harness kill that downstream reads as "no findings". -_gstack_codex_timeout_wrapper 330 codex review --base -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" {{CODEX_REVIEW_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} < /dev/null >"$TMPOUT" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout 330 codex review --base -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" {{CODEX_REVIEW_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} < /dev/null >"$TMPOUT" 2>"$TMPERR" _CODEX_EXIT=$? cat "$TMPOUT" if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "330" - _gstack_codex_log_hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "330" + "$_CODEX_PROBE" log-hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 5.5 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/." elif [ "$_CODEX_EXIT" != "0" ]; then # Surface non-zero exits (parse errors, arg-shape breaks, etc.) so the @@ -63,7 +62,7 @@ elif [ "$_CODEX_EXIT" != "0" ]; then # burn 30-60min misdiagnosing it. echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")" head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true - _gstack_codex_log_event "codex_nonzero_exit" "review:$_CODEX_EXIT" + "$_CODEX_PROBE" log-event codex_nonzero_exit "review:$_CODEX_EXIT" fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex review' --exit "$_CODEX_EXIT" --stderr "$TMPERR" structured "$TMPOUT" ``` @@ -94,8 +93,7 @@ cd "$_REPO_ROOT" FOCUS_FILE="$_REPO_ROOT/.gstack/tmp/" [ -s "$FOCUS_FILE" ] || { echo "Not run: $FOCUS_FILE is missing or empty, so the focus text was never written. Write it, then run this block again." >&2; exit 1; } ~/.claude/skills/gstack/bin/gstack-review-log --start codex-review -source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1 -_gstack_codex_select_model exec || exit 1 +{{CODEX_SELECT:exec}} _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; } { printf '%s\n' "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. Do not invoke any installed skill (Codex home skills/, .agents/); answer directly. Do NOT modify agents/openai.yaml. Stay focused on repository code only." @@ -107,13 +105,13 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp git diff "...HEAD" 2>/dev/null printf '\nDIFF_END\n' } > "$_PROMPT_FILE" -_gstack_codex_timeout_wrapper 330 codex exec - -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout 330 codex exec - -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" _CODEX_EXIT=$? rm -f "$_PROMPT_FILE" "$FOCUS_FILE" cat "$TMPOUT" if [ "$_CODEX_EXIT" = "124" ]; then - _gstack_codex_log_event "codex_timeout" "330" - _gstack_codex_log_hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" + "$_CODEX_PROBE" log-event codex_timeout "330" + "$_CODEX_PROBE" log-hang "review" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)" echo "Codex stalled past 5.5 minutes." fi bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex review' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPOUT.events" structured "$TMPOUT" diff --git a/design-consultation/sections/proposal-and-preview.md b/design-consultation/sections/proposal-and-preview.md index 31851e897..1b9d39584 100644 --- a/design-consultation/sections/proposal-and-preview.md +++ b/design-consultation/sections/proposal-and-preview.md @@ -178,11 +178,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/design-review/SKILL.md b/design-review/SKILL.md index 1530bd59c..2332b5a83 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -1678,11 +1678,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2c0c6682c..ed3e8a224 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -32,27 +32,37 @@ P0 or P1 finding blocks exactly like a native P0/P1. `unverified` and the readiness dashboard and the PR body, and never count it as a pass. -### `gstack: cannot locate gstack-codex-probe (shell: ...)` / `CODEX_MODE: helper_unavailable` +### `gstack: cannot load gstack-codex-probe` / `gstack: cannot locate (shell: ...)` / `CODEX_MODE: helper_unavailable` -**Meaning.** Skill blocks load gstack's shell helpers (`gstack-codex-probe`, -`gstack-egress-lib.sh`) into the shell your agent runs. A helper finds its own +**Meaning.** Skills run `gstack-codex-probe` as a command, one subcommand per +check, so the shell your agent uses does not matter. `cannot load +gstack-codex-probe` and `CODEX_MODE: helper_unavailable` mean the probe file is +missing or not executable in your install. + +`cannot locate (shell: ...)` comes from a helper that is still loaded +into your shell with `source` (`gstack-egress-lib.sh`, and the Codex probe when +a skill rendered before the upgrade sources it). Such a helper finds its own directory from bash (`BASH_SOURCE`) or zsh (`%x`). In any other shell (dash, -sh), or when the shell cannot say which file it is reading, the helper stops -instead of guessing a path. The message names the shell it saw. +sh), or when the shell cannot say which file it is reading, it stops instead of +guessing a path. The message names the shell it saw. -**Fix.** Run the skill from bash or zsh (the macOS and Linux defaults). If -the shell cannot be changed, tell the helper where gstack is installed: +**Fix.** For `cannot load`, re-run `./setup` from your gstack checkout (or +`/gstack-upgrade`). For `cannot locate`, run the skill from bash or zsh (the +macOS and Linux defaults), or tell the helper where gstack is installed: ```bash export GSTACK_ROOT=~/.claude/skills/gstack # your install dir; it holds bin/ ``` -If the message says `cannot load ...`, the helper file is missing: re-run -`./setup` from your gstack checkout. +If you see `gstack: sourcing gstack-codex-probe is deprecated ...`, a skill +rendered before the upgrade is still sourcing the probe. It keeps working +until a release on or after 2026-10-21; run `/gstack-upgrade` to re-render +your skills now. -**Expected result.** `zsh -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec'` -prints `CODEX_MODEL: (exec; source: ...)`, and preflights print a -`CODEX_MODE` other than `helper_unavailable`. +**Expected result.** `~/.claude/skills/gstack/bin/gstack-codex-probe select-model exec` +prints `CODEX_SEL: ` (and `CODEX_MODEL: (exec; source: ...)` on +stderr) from any shell, and preflights print a `CODEX_MODE` other than +`helper_unavailable`. `gstack-codex-probe help` lists every subcommand. ### `Codex outside review unavailable: Codex's sandbox could not start here (...)` diff --git a/document-release/sections/release-body.md b/document-release/sections/release-body.md index 470571529..e5b298117 100644 --- a/document-release/sections/release-body.md +++ b/document-release/sections/release-body.md @@ -222,26 +222,30 @@ health summary and continue to Step 9. Ship-owned children already stopped at St ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -250,12 +254,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -263,7 +267,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`." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Fall back to the Claude subagent path. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Fall back to the Claude subagent path. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Fall back to the Claude subagent path. - **`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. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. @@ -346,11 +350,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index ad39c5541..3daa945f6 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -840,11 +840,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } @@ -1200,11 +1203,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/plan-ceo-review/sections/review-sections.md b/plan-ceo-review/sections/review-sections.md index 1c0ea2b2c..0f869cf3b 100644 --- a/plan-ceo-review/sections/review-sections.md +++ b/plan-ceo-review/sections/review-sections.md @@ -357,26 +357,30 @@ review. The user turns this off only by asking explicitly ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -385,12 +389,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -398,7 +402,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). Skip the reviewer invocation; record disabled coverage as directed below; 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`." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Fall back to the Claude subagent path. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Fall back to the Claude subagent path. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Fall back to the Claude subagent path. - **`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 construct the prompt below, then follow **Native fallback**. Conflicting inherited harness markers are not grounds to guess another provider. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. @@ -505,11 +509,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index deb5e1031..14d0dd717 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -1050,11 +1050,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/plan-devex-review/sections/review-sections.md b/plan-devex-review/sections/review-sections.md index 43ca64c72..037429cb9 100644 --- a/plan-devex-review/sections/review-sections.md +++ b/plan-devex-review/sections/review-sections.md @@ -296,26 +296,30 @@ review. The user turns this off only by asking explicitly ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -324,12 +328,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -337,7 +341,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`." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Fall back to the Claude subagent path. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Fall back to the Claude subagent path. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Fall back to the Claude subagent path. - **`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. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. @@ -439,11 +443,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/plan-eng-review/sections/review-sections.md b/plan-eng-review/sections/review-sections.md index c7f555a02..394a44d12 100644 --- a/plan-eng-review/sections/review-sections.md +++ b/plan-eng-review/sections/review-sections.md @@ -799,26 +799,30 @@ review. The user turns this off only by asking explicitly ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -827,12 +831,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -840,7 +844,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). Skip the reviewer invocation; record disabled coverage as directed below; 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`." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Fall back to the Claude subagent path. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Fall back to the Claude subagent path. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Fall back to the Claude subagent path. - **`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 construct the prompt below, then follow **Native fallback**. Conflicting inherited harness markers are not grounds to guess another provider. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. @@ -944,11 +948,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/review/sections/adversarial.md b/review/sections/adversarial.md index acdd1a74b..286157bc3 100644 --- a/review/sections/adversarial.md +++ b/review/sections/adversarial.md @@ -18,27 +18,30 @@ echo "DIFF_SIZE: $DIFF_TOTAL" ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe review; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec review; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -47,12 +50,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -60,7 +63,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." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Keep the required Claude adversarial pass; do not dispatch a duplicate. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Keep the required Claude adversarial pass; do not dispatch a duplicate. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Keep the required Claude adversarial pass; do not dispatch a duplicate. - **`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. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Keep the required Claude adversarial pass; do not dispatch a duplicate. @@ -131,11 +134,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } @@ -190,11 +196,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" : >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model review || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model review) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex review --base '' -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" -c "review_model=\"${_GSTACK_CODEX_SEL:?}\"" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null >"$_OUTSIDE_TMP/text" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex review --base '' -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "review_model=\"${_CODEX_SEL:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null >"$_OUTSIDE_TMP/text" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/scripts/resolvers/constants.ts b/scripts/resolvers/constants.ts index dc0bf435d..d5fa66f6d 100644 --- a/scripts/resolvers/constants.ts +++ b/scripts/resolvers/constants.ts @@ -60,12 +60,12 @@ export const CODEX_WEB_SEARCH_FLAG = `-c 'web_search="cached"'`; * Default model for gstack-owned Codex invocations when nothing else chooses. * * The runtime model is resolved per invocation kind by - * `_gstack_codex_select_model exec|review` (bin/gstack-codex-probe, backed by + * `gstack-codex-probe select-model exec|review` (backed by * resolveCodexRuntimeModel in scripts/resolve-codex-generation-model.ts): - * explicit request, then GSTACK_CODEX_MODEL, then Codex config.toml (`model`; - * `review_model` first for native review; honors CODEX_HOME), then this default - * (#2914). The selection is printed before the first paid call and the probe - * checks the same record the flags below pass. + * explicit request (`--model`), then GSTACK_CODEX_MODEL, then Codex config.toml + * (`model`; `review_model` first for native review; honors CODEX_HOME), then this + * default (#2914). The selection is printed before the first paid call and the + * probe checks the same record the flags below pass. */ export const CODEX_FRONTIER_MODEL = 'gpt-6-astra'; /** @@ -74,11 +74,29 @@ export const CODEX_FRONTIER_MODEL = 'gpt-6-astra'; * skill workflow inside its budget (#2847). Read-only sandboxes do not prevent this. */ export const CODEX_SKILLS_ISOLATION_FLAG = '-c skills.include_instructions=false'; -const SELECTED_MODEL = '${_GSTACK_CODEX_SEL:?}'; -/** Requires `_gstack_codex_select_model exec` earlier in the same shell; `:?` stops an unselected command. */ +/** Captured from `select-model` by codexSelect(); `:?` stops an unselected command. */ +const SELECTED_MODEL = '${_CODEX_SEL:?}'; +/** The sandbox codexSelect() captured; read-only unless GSTACK_CODEX_NO_SANDBOX=1. */ +export const CODEX_SANDBOX_REF = '${_CODEX_SANDBOX_MODE:?}'; +/** Requires codexSelect('exec') earlier in the same block. */ export const CODEX_MODEL_CONFIG_FLAG = `-c "model=\\"${SELECTED_MODEL}\\"" ${CODEX_SKILLS_ISOLATION_FLAG}`; -/** Requires `_gstack_codex_select_model review`; native review prefers review_model, so both carry the selection. */ +/** Requires codexSelect('review'); native review prefers review_model, so both carry the selection. */ export const CODEX_REVIEW_MODEL_CONFIG_FLAG = `-c "review_model=\\"${SELECTED_MODEL}\\"" ${CODEX_MODEL_CONFIG_FLAG}`; +/** The executed probe; pathRewrites map the Claude path per host. */ +export const CODEX_PROBE_PATH = '~/.claude/skills/gstack/bin/gstack-codex-probe'; + +/** + * Runs `gstack-codex-probe select-model` and captures its KEY: value lines into + * `_CODEX_SEL` and `_CODEX_SANDBOX_MODE` for CODEX_MODEL_CONFIG_FLAG and + * CODEX_SANDBOX_REF. The probe is executed, never sourced, so no shell state + * crosses from the helper and the calling shell does not matter. + */ +export function codexSelect(kind: 'exec' | 'review', probe: string = CODEX_PROBE_PATH): string { + return `_CODEX_PROBE=${probe} +_CODEX_OUT=$("$_CODEX_PROBE" select-model ${kind}) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p')`; +} /** * Shared Codex error handling block for resolver output. @@ -99,20 +117,20 @@ On any error: continue — ${feature} is informational, not a gate.`; * 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: + * Emits ONE self-contained bash block that runs `gstack-codex-probe` as a + * command (one subcommand per check), so it works from bash, zsh or sh and + * nothing it learns persists to later blocks except the echoed mode. It: * 1. reads the `codex_reviews` master switch, - * 2. sources `gstack-codex-probe` with POSIX `.`, so even sh/dash reach the - * helper's own diagnostic (`CODEX_MODE: helper_unavailable`), + * 2. checks that the probe is installed and executable + * (`CODEX_MODE: helper_unavailable` with the fix otherwise), * 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, + * then `check-auth`, `check-sandbox`, `probe-model` and `check-version`, + * 4. logs the relevant `log-event` for each non-ready outcome, * 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) | `helper_unavailable` (the probe could - * not be sourced; its own diagnostic is echoed) | `not_installed` | `not_authed` | + * Mode values: `disabled` (config off) | `helper_unavailable` (the probe is + * missing or not executable) | `not_installed` | `not_authed` | * `broken_install` | `sandbox_unavailable` | `model_unusable` | `quota_exhausted` | * `unverified` (echoed as `unverified (rate_limited)` after a probe-time 429) | `ready`. * The path is host-rewritten at gen-skill-docs time (pathRewrites), so the @@ -135,27 +153,30 @@ export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip ? 'Keep the required Claude adversarial pass; do not dispatch a duplicate.' : 'Fall back to the Claude subagent path.'; 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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=${CODEX_PROBE_PATH} _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="\${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then ${m}="disabled" elif { [ -n "\${CODEX_THREAD_ID:-}" ] || [ -n "\${CODEX_SANDBOX:-}" ] || [ "\${GSTACK_ACTIVE_HOST:-}" = codex ]; }; 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 + ${m}="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then ${m}="helper_unavailable"; echo "$_gstack_helper_error" -elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then - ${m}="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + ${m}="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; }${opts.nativeReview ? ` - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe review; _CODEX_MP=$?; }` : ''} + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec${opts.nativeReview ? ' review' : ''}; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then ${m}="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -164,12 +185,12 @@ else ${m}="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then ${m}="model_unusable" - elif [ "\${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then ${m}="unverified" - elif [ "\${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then ${m}="unverified (rate_limited)" else - ${m}="ready"; _gstack_codex_version_check 2>/dev/null || true + ${m}="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $${m}" @@ -177,7 +198,7 @@ echo "CODEX_MODE: $${m}" Branch on the echoed \`CODEX_MODE\`: - **\`disabled\`** — the user turned Codex reviews off (\`codex_reviews=disabled\`). ${disabledLine} -- **\`helper_unavailable\`** — the helper could not load; relay the line above (cause and fix). ${nativeRoute} +- **\`helper_unavailable\`** — the probe is missing or not executable; relay the line above (cause and fix). ${nativeRoute} - **\`not_installed\`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: \`npm install -g @openai/codex\`." ${nativeRoute} - **\`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. - **\`not_authed\`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run \`codex login\` or set \`$CODEX_API_KEY\`." ${nativeRoute} diff --git a/scripts/resolvers/index.ts b/scripts/resolvers/index.ts index 4c9780d0d..4ee4174dc 100644 --- a/scripts/resolvers/index.ts +++ b/scripts/resolvers/index.ts @@ -39,7 +39,7 @@ import { generateTasksSectionEmit, generateTasksSectionAggregate } from './tasks import { SECTION, SECTION_INDEX } from './sections'; import { generateRedactInvocationBlock } from './redact-doc'; import { generateFreeTextFile } from './free-text-file'; -import { FOREGROUND_DISPATCH_NOTE } from './constants'; +import { FOREGROUND_DISPATCH_NOTE, codexSelect } from './constants'; import { generateThirdPartyActions } from './third-party-actions'; import { generateAsideSetup, generateAsideCookbook, generateAsideResearch, generateUntrustedContentWarning, asideExecPrelude, asideResearchSend } from './aside'; import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup, generateBrowseFallback } from './browse'; @@ -65,6 +65,7 @@ export const RESOLVERS: Record = { CODEX_WEB_SEARCH_FLAG: generateCodexWebSearchFlag, CODEX_MODEL_CONFIG_FLAG: generateCodexModelConfigFlag, CODEX_REVIEW_MODEL_CONFIG_FLAG: generateCodexReviewModelConfigFlag, + CODEX_SELECT: (_ctx, args) => codexSelect(args?.[0] === 'review' ? 'review' : 'exec'), CLAUDE_MODEL_FLAG: generateClaudeModelFlag, REDACT_INVOCATION_BLOCK: generateRedactInvocationBlock, FREE_TEXT_FILE: generateFreeTextFile, diff --git a/scripts/resolvers/outside-voice.ts b/scripts/resolvers/outside-voice.ts index e741c9188..bc3037488 100644 --- a/scripts/resolvers/outside-voice.ts +++ b/scripts/resolvers/outside-voice.ts @@ -2,7 +2,7 @@ * Callers own prompts, opt-in rules, timeouts, gates, and native fallbacks. */ import { toShellPath, type TemplateContext } from './types'; -import { CODEX_MODEL_CONFIG_FLAG, CODEX_REVIEW_MODEL_CONFIG_FLAG, CODEX_WEB_SEARCH_FLAG, codexPreflight } from './constants'; +import { CODEX_MODEL_CONFIG_FLAG, CODEX_REVIEW_MODEL_CONFIG_FLAG, CODEX_SANDBOX_REF, CODEX_WEB_SEARCH_FLAG, codexPreflight, codexSelect } from './constants'; import { runtimeRootPrelude } from './runtime-root'; export function outsideVoiceFor(ctx: Pick) { @@ -106,14 +106,14 @@ export function outsideVoiceCommand(ctx: TemplateContext, opts: OutsideCommandOp const root = toShellPath(ctx.paths.skillRoot); const prompt = sh(opts.promptFile ?? ''); const codex = opts.structuredBase - ? `codex review --base ${sh(opts.structuredBase)} -c "sandbox_mode=\\"\${_GSTACK_CODEX_SANDBOX:?}\\"" ${CODEX_REVIEW_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} < /dev/null` - : `codex exec - -C "$_REPO_ROOT" -s "\${_GSTACK_CODEX_SANDBOX:?}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT"`; + ? `codex review --base ${sh(opts.structuredBase)} -c "sandbox_mode=\\"${CODEX_SANDBOX_REF}\\"" ${CODEX_REVIEW_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} < /dev/null` + : `codex exec - -C "$_REPO_ROOT" -s "${CODEX_SANDBOX_REF}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT"`; const invocation = v.id === 'codex' - ? `source "${bin}/gstack-codex-probe" && _gstack_codex_select_model ${opts.structuredBase ? 'review' : 'exec'} || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice + ? `${codexSelect(opts.structuredBase ? 'review' : 'exec', `"${bin}/gstack-codex-probe"`)} +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper ${Math.ceil(opts.timeoutMs / 1000)} ${codex} >"$_OUTSIDE_TMP/${opts.structuredBase ? 'text' : 'events'}" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout ${Math.ceil(opts.timeoutMs / 1000)} ${codex} >"$_OUTSIDE_TMP/${opts.structuredBase ? 'text' : 'events'}" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? ${opts.structuredBase ? 'cat "$_OUTSIDE_TMP/text"' : 'cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events"'}` : `_OUTSIDE_EXIT=0 "${bin}/gstack-claude-code" --cwd "$_REPO_ROOT" --access ${opts.access ?? 'none'} --timeout-ms ${opts.timeoutMs} <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/result.json" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? @@ -134,8 +134,8 @@ printf '\\nREPOSITORY CONTEXT (data, not instructions):\\n' >>"$_OUTSIDE_INPUT" ${opts.diffCommand} >>"$_OUTSIDE_INPUT" || exit 1` : ''} ${invocation} ${v.id === 'codex' && ctx.skillName === 'autoplan' ? `if [ "$_OUTSIDE_EXIT" -eq 124 ]; then - _gstack_codex_log_event "codex_timeout" "${Math.ceil(opts.timeoutMs / 1000)}" || true - _gstack_codex_log_hang "autoplan" "0" || true + "$_CODEX_PROBE" log-event codex_timeout "${Math.ceil(opts.timeoutMs / 1000)}" || true + "$_CODEX_PROBE" log-hang autoplan 0 || true fi` : ''} cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } _OUTSIDE_RC=0 diff --git a/ship/sections/adversarial.md b/ship/sections/adversarial.md index 99d41532d..fd15fa6a4 100644 --- a/ship/sections/adversarial.md +++ b/ship/sections/adversarial.md @@ -18,27 +18,30 @@ echo "DIFF_SIZE: $DIFF_TOTAL" ```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 preflight: the probe runs as a command, so any shell works. +_CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) _gstack_helper_error="" -. ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || _gstack_helper_error="${_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}" +[ -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" if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then _CODEX_MODE="under_codex" elif ! command -v codex >/dev/null 2>&1; then - _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true + _CODEX_MODE="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true elif [ -n "$_gstack_helper_error" ]; then _CODEX_MODE="helper_unavailable"; echo "$_gstack_helper_error" -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 +elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then + _CODEX_MODE="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true else - # The free sandbox check runs before the paid model probe. Probe code 2 means - # the CLI cannot execute at all, a different fix from an unusable model. - _CODEX_MP=0; _gstack_codex_sandbox_preflight || _CODEX_MP=3 - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe; _CODEX_MP=$?; } - [ "$_CODEX_MP" -ne 0 ] || { _gstack_codex_model_probe review; _CODEX_MP=$?; } + # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run. + _CODEX_MP=0; _CODEX_PS="" + "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3 + for _CODEX_KIND in exec review; do + [ "$_CODEX_MP" -eq 0 ] || break + _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\n' "$_CODEX_PO" + case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac + done if [ "$_CODEX_MP" -eq 3 ]; then _CODEX_MODE="sandbox_unavailable" elif [ "$_CODEX_MP" -eq 2 ]; then @@ -47,12 +50,12 @@ else _CODEX_MODE="quota_exhausted" elif [ "$_CODEX_MP" -ne 0 ]; then _CODEX_MODE="model_unusable" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = inconclusive ]; then + elif [ "$_CODEX_PS" = inconclusive ]; then _CODEX_MODE="unverified" - elif [ "${_GSTACK_CODEX_PROBE_STATE:-}" = rate_limited ]; then + elif [ "$_CODEX_PS" = rate_limited ]; then _CODEX_MODE="unverified (rate_limited)" else - _CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true + _CODEX_MODE="ready"; "$_CODEX_PROBE" check-version || true fi fi echo "CODEX_MODE: $_CODEX_MODE" @@ -60,7 +63,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." -- **`helper_unavailable`** — the helper could not load; relay the line above (cause and fix). Keep the required Claude adversarial pass; do not dispatch a duplicate. +- **`helper_unavailable`** — the probe is missing or not executable; relay the line above (cause and fix). Keep the required Claude adversarial pass; do not dispatch a duplicate. - **`not_installed`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: `npm install -g @openai/codex`." Keep the required Claude adversarial pass; do not dispatch a duplicate. - **`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. - **`not_authed`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run `codex login` or set `$CODEX_API_KEY`." Keep the required Claude adversarial pass; do not dispatch a duplicate. @@ -131,11 +134,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } @@ -190,11 +196,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" : >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model review || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model review) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 540 codex review --base '' -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" -c "review_model=\"${_GSTACK_CODEX_SEL:?}\"" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null >"$_OUTSIDE_TMP/text" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 540 codex review --base '' -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "review_model=\"${_CODEX_SEL:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null >"$_OUTSIDE_TMP/text" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md index a1914723d..9564d545f 100644 --- a/ship/sections/review-army.md +++ b/ship/sections/review-army.md @@ -183,11 +183,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 300 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 300 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/spec/sections/gate-and-file.md b/spec/sections/gate-and-file.md index 65efe4a4a..474ec6121 100644 --- a/spec/sections/gate-and-file.md +++ b/spec/sections/gate-and-file.md @@ -178,11 +178,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT _OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt" cat -- '' >"$_OUTSIDE_INPUT" || exit 1 -source "$HOME/.claude/skills/gstack/bin/gstack-codex-probe" && _gstack_codex_select_model exec || exit 1 -_gstack_codex_sandbox_preflight >/dev/null || exit 1 -_gstack_codex_first_use_notice +_CODEX_PROBE="$HOME/.claude/skills/gstack/bin/gstack-codex-probe" +_CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1 +_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p') +_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p') +"$_CODEX_PROBE" check-sandbox || exit 1 +"$_CODEX_PROBE" show-first-use-notice _OUTSIDE_EXIT=0 -_gstack_codex_timeout_wrapper 120 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" -c "model=\"${_GSTACK_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? +"$_CODEX_PROBE" run-with-timeout 120 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/events" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$? cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events" cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; } diff --git a/test/autoplan-dual-voice-evidence.test.ts b/test/autoplan-dual-voice-evidence.test.ts index 85c88ed26..037bd18ec 100644 --- a/test/autoplan-dual-voice-evidence.test.ts +++ b/test/autoplan-dual-voice-evidence.test.ts @@ -263,8 +263,8 @@ test.each(['assignment-only','set-config','other-config','foreign-reader','or-co if(kind==='inside-body')input.command=input.command.replace(guards+'\n','').replace('_OUTSIDE_EXIT=0','_OUTSIDE_EXIT=0\n'+guards); if(kind==='before-cd')input.command=guards+'\ncd '+f.dir+'\n'+f.options.commands.outside.replace("''","'"+f.file+"'"); if(kind==='changed-harness')input.command=input.command.replace('exit 78','exit 0'); - if(kind==='changed-timeout')input.command=input.command.replace('_gstack_codex_timeout_wrapper 540','_gstack_codex_timeout_wrapper 1'); - if(kind==='changed-sandbox')input.command=input.command.replace('-s "${_GSTACK_CODEX_SANDBOX:?}"','-s danger-full-access'); + if(kind==='changed-timeout')input.command=input.command.replace('run-with-timeout 540','run-with-timeout 1'); + if(kind==='changed-sandbox')input.command=input.command.replace('-s "${_CODEX_SANDBOX_MODE:?}"','-s danger-full-access'); if(kind==='changed-prompt')input.command=input.command.replace('codex exec - ','codex exec "Different plan" '); if(kind==='skipped-validator')input.command=input.command.replace(/^bun .*outside-review-result.*\n/m,''); if(kind==='suffix')input.command+='\ntrue'; diff --git a/test/binding-template-drift.test.ts b/test/binding-template-drift.test.ts index 84f7f535e..5c4c5065a 100644 --- a/test/binding-template-drift.test.ts +++ b/test/binding-template-drift.test.ts @@ -120,7 +120,7 @@ describe('content-binding template drift', () => { const codex = rendered('codex/sections/review-mode.md'); const starts = [...codex.matchAll(/gstack-review-log --start codex-review/g)]; expect(starts).toHaveLength(2); - expect(starts[0].index).toBeLessThan(codex.indexOf('_gstack_codex_timeout_wrapper 330 codex review')); + expect(starts[0].index).toBeLessThan(codex.indexOf('run-with-timeout 330 codex review')); expect(starts[1].index).toBeLessThan(codex.indexOf('git diff "...HEAD"')); expect(codex).toContain('--finish CODEX_REVIEW_START'); expect(codex).toContain('"completed":COMPLETED,"converged":CONVERGED'); diff --git a/test/codex-hardening.test.ts b/test/codex-hardening.test.ts index 0a76657d7..e42c916e7 100644 --- a/test/codex-hardening.test.ts +++ b/test/codex-hardening.test.ts @@ -645,20 +645,20 @@ describe('codex timeout wrapper: /review + /ship diff passes', () => { test(`${relPath}: both diff-review Codex calls run under the wrapper`, () => { const wrapped = - read().match(/_gstack_codex_timeout_wrapper\s+\d+\s+codex\s+(exec|review)\b/g) ?? []; + read().match(/run-with-timeout\s+\d+\s+codex\s+(exec|review)\b/g) ?? []; // Adversarial pass + structured review pass. expect(wrapped.length).toBeGreaterThanOrEqual(2); }); test(`${relPath}: does not claim \`timeout\` is unavailable on macOS`, () => { - // _gstack_codex_timeout_wrapper resolves gtimeout -> timeout -> unwrapped, + // run-with-timeout resolves gtimeout -> timeout -> a bash watchdog, // so the coreutils-less case is already handled. The old claim is what // steered these call sites away from the wrapper in the first place. expect(read()).not.toMatch(/doesn't exist on macOS/); }); test(`${relPath}: wrapper budget stays under the outer Bash gate`, () => { - const budgets = [...read().matchAll(/_gstack_codex_timeout_wrapper\s+(\d+)\s+codex\b/g)].map( + const budgets = [...read().matchAll(/run-with-timeout\s+(\d+)\s+codex\b/g)].map( (m) => Number(m[1]) * 1000, ); expect(budgets.length).toBeGreaterThan(0); @@ -710,13 +710,13 @@ describe('codex skeleton+sections union: review sandbox + fail-closed gate + tim test(`${relPath}: (a) every scoped codex review invocation pins sandbox_mode from the selected sandbox`, () => { const invocations = read() .split('\n') - .filter((l) => /_gstack_codex_timeout_wrapper\s+\d+\s+codex\s+review\b/.test(l)); + .filter((l) => /run-with-timeout\s+\d+\s+codex\s+review\b/.test(l)); expect(invocations.length).toBeGreaterThanOrEqual(1); for (const line of invocations) { - // _gstack_codex_select_model sets read-only (full access only for + // select-model reports read-only (full access only for // GSTACK_CODEX_NO_SANDBOX=1, test/codex-model-probe.test.ts); :? stops // an unselected command instead of inheriting config.toml's default. - expect(line).toContain('sandbox_mode=\\"${_GSTACK_CODEX_SANDBOX:?}\\"'); + expect(line).toContain('sandbox_mode=\\"${_CODEX_SANDBOX_MODE:?}\\"'); // `codex review` has no -s/--sandbox flag (verified 0.147.0) — the // config override is the only lever. `-s read-only` here would fail // at argv parsing, which check (b) would then read as a gate FAIL. @@ -766,13 +766,13 @@ describe('codex skeleton+sections union: review sandbox + fail-closed gate + tim test(`${relPath}: (c) every Bash gate sits strictly above its section's wrapper budgets`, () => { // Split on `## ` headings; within any section that declares BOTH a Bash // tool gate (`timeout: N` in ms) and a wrapper budget - // (`_gstack_codex_timeout_wrapper S codex`), every gate must be strictly + // (`run-with-timeout S codex`), every gate must be strictly // greater than every wrapper budget so the wrapper fires first. const sections = read().split(/\n## /); const inspected: string[] = []; for (const section of sections) { const gates = [...section.matchAll(/timeout:\s*(\d{4,})/g)].map((m) => Number(m[1])); - const wrappers = [...section.matchAll(/_gstack_codex_timeout_wrapper\s+(\d+)\s+codex\b/g)].map( + const wrappers = [...section.matchAll(/run-with-timeout\s+(\d+)\s+codex\b/g)].map( (m) => Number(m[1]) * 1000, ); if (gates.length === 0 || wrappers.length === 0) continue; @@ -941,17 +941,17 @@ describe('codex broken-install detection (#2742)', () => { for (const rel of ['autoplan/SKILL.md.tmpl', 'autoplan/SKILL.md']) { const raw = fs.readFileSync(path.join(ROOT, rel), 'utf-8'); const src = rel.endsWith('.tmpl') ? raw.replace('{{OUTSIDE_PREFLIGHT:autoplan}}', RESOLVERS.OUTSIDE_PREFLIGHT({ host: 'claude', paths: HOST_PATHS.claude, skillName: 'autoplan', tmplPath: rel }, ['autoplan'])) : raw; - expect(src).toContain('_gstack_codex_model_probe; _CODEX_MP=$?'); + expect(src).toContain('_CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?'); expect(src).toMatch(/_CODEX_MP" -eq 2/); expect(src).toContain('binary cannot run'); - expect(src).not.toContain('elif ! _gstack_codex_model_probe'); + expect(src).not.toContain('elif ! "$_CODEX_PROBE" probe-model'); } }); test('the preflight resolver routes exit 2 to broken_install', () => { const src = fs.readFileSync(path.join(ROOT, 'scripts/resolvers/constants.ts'), 'utf8'); expect(src).toContain('broken_install'); - // The chain must capture the probe's code; `elif ! _gstack_codex_model_probe` + // The chain must capture the probe's code; `elif ! gstack-codex-probe probe-model` // collapses 1 and 2 into one branch and loses the distinction. expect(src).toContain('_CODEX_MP=$?'); }); diff --git a/test/codex-invocation-sites.test.ts b/test/codex-invocation-sites.test.ts index 98c267739..3fce944a7 100644 --- a/test/codex-invocation-sites.test.ts +++ b/test/codex-invocation-sites.test.ts @@ -2,7 +2,7 @@ * B1 static coverage: every rendered fence that runs `codex exec` or * `codex review` as a command routes the result through the shared validator * (lib/outside-review-result.ts) later in the same fence, and takes its - * sandbox from `_GSTACK_CODEX_SANDBOX` (read-only unless GSTACK_CODEX_NO_SANDBOX=1); + * sandbox from `_CODEX_SANDBOX_MODE` (read-only unless GSTACK_CODEX_NO_SANDBOX=1); * every `codex exec` reads its prompt on stdin (E5). * Prose mentions of the commands are ignored: only command positions count. */ @@ -16,8 +16,8 @@ const ROOT = path.resolve(import.meta.dir, '..'); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-sites-')); afterAll(() => fs.rmSync(TMP, { recursive: true, force: true })); -/** `codex exec|review` at the start of a shell command, optionally behind the timeout wrapper. */ -const COMMAND = /(?:^|[;&|]\s*|\$\(\s*)(?:_gstack_codex_timeout_wrapper\s+\d+\s+)?codex\s+(?:exec|review)\b/; +/** `codex exec|review` at the start of a shell command, optionally behind the probe's run-with-timeout. */ +const COMMAND = /(?:^|[;&|]\s*|\$\(\s*)(?:"\$_CODEX_PROBE"\s+run-with-timeout\s+\d+\s+)?codex\s+(?:exec|review)\b/; const VALIDATOR = /outside-review-result\.ts\b/; interface Site { file: string; line: string; fence: string; after: string } @@ -74,8 +74,8 @@ describe('every rendered Codex invocation is classified by the shared validator' expect(argv).toEqual([]); }); - test(`${host}: each codex exec/review command takes its sandbox from _GSTACK_CODEX_SANDBOX`, () => { - const literal = rendered[host]!.filter(site => !site.line.includes('${_GSTACK_CODEX_SANDBOX:?}')).map(s => `${s.file}: ${s.line}`); + test(`${host}: each codex exec/review command takes its sandbox from _CODEX_SANDBOX_MODE`, () => { + const literal = rendered[host]!.filter(site => !site.line.includes('${_CODEX_SANDBOX_MODE:?}')).map(s => `${s.file}: ${s.line}`); expect(literal).toEqual([]); }); } @@ -86,13 +86,13 @@ describe('every rendered Codex invocation is classified by the shared validator' '```bash', '# codex exec in a comment', 'echo "use codex exec later"', - '_gstack_codex_timeout_wrapper 30 codex exec "x" -s read-only', + '"$_CODEX_PROBE" run-with-timeout 30 codex exec "x" -s read-only', '```', ].join('\n'); const sites = codexSites(doc, 'planted.md'); - expect(sites.map(s => s.line)).toEqual(['_gstack_codex_timeout_wrapper 30 codex exec "x" -s read-only']); + expect(sites.map(s => s.line)).toEqual(['"$_CODEX_PROBE" run-with-timeout 30 codex exec "x" -s read-only']); expect(VALIDATOR.test(sites[0]!.after)).toBe(false); - expect(sites[0]!.line.includes('${_GSTACK_CODEX_SANDBOX:?}')).toBe(false); + expect(sites[0]!.line.includes('${_CODEX_SANDBOX_MODE:?}')).toBe(false); expect(codexSites('```bash\nX=$(codex exec "y")\n```', 'p.md')).toHaveLength(1); }); }); diff --git a/test/codex-model-probe.test.ts b/test/codex-model-probe.test.ts index 3f35a5f61..08ce9a888 100644 --- a/test/codex-model-probe.test.ts +++ b/test/codex-model-probe.test.ts @@ -34,6 +34,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { codexPreflight } from '../scripts/resolvers/constants'; +import { withoutDeprecation } from './helpers/codex-probe-sourcing'; const ROOT = path.resolve(import.meta.dir, '..'); const PROBE = path.join(ROOT, 'bin', 'gstack-codex-probe'); @@ -129,7 +130,7 @@ function runProbe(f: Fixture, stubMode: string, extraEnv: Record timeout: 10000, }, ); - return { stdout: (result.stdout ?? '').toString(), stderr: (result.stderr ?? '').toString(), status: result.status ?? -1 }; + return { stdout: (result.stdout ?? '').toString(), stderr: withoutDeprecation((result.stderr ?? '').toString()), status: result.status ?? -1 }; } function lastArgs(f: Fixture): string { diff --git a/test/codex-probe-subcommands.test.ts b/test/codex-probe-subcommands.test.ts new file mode 100644 index 000000000..5dfed8e26 --- /dev/null +++ b/test/codex-probe-subcommands.test.ts @@ -0,0 +1,280 @@ +/** + * bin/gstack-codex-probe is an executed command: one subcommand per probe, + * KEY: value lines on stdout, human lines where they went before, nothing set + * in the caller's shell. Generated skills run it as a command, so the calling + * shell (bash, zsh, sh) no longer matters. Sourcing still works during the + * compatibility window and says it is deprecated; executing never does. + */ +import { describe, test, expect } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { PROBE_SOURCING_DEPRECATION } from './helpers/codex-probe-sourcing'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const PROBE = path.join(ROOT, 'bin', 'gstack-codex-probe'); +const HAS_ZSH = Boolean(Bun.which('zsh')); +const ZSH_REQUIRED = Boolean(process.env.CI) && process.platform !== 'win32'; +const SHELLS = ['bash', 'zsh'] as const; +const SUBCOMMANDS = ['select-model', 'check-auth', 'show-sandbox', 'check-sandbox', 'probe-model', 'check-version', + 'show-first-use-notice', 'run-with-timeout', 'log-event', 'log-hang', 'help']; + +const STUB = `#!/usr/bin/env bash +if [ "$1" = sandbox ]; then + [ "\${STUB_SANDBOX:-ok}" = ok ] && exit 0 + echo 'bwrap: No permissions to create new namespace' >&2; exit 1 +fi +case "$1" in --version) echo "codex-cli \${STUB_VERSION:-0.160.1}"; exit 0 ;; esac +printf '%s\\n' "$*" >> "$STUB_ARGS_LOG" +case "\${STUB_MODE:-ok}" in + ok) echo OK; exit 0 ;; + model400) echo 'ERROR: {"type":"error","status":400,"error":{"message":"The model is not supported when using Codex with a ChatGPT account."}}' >&2; exit 1 ;; + broken) echo 'Error: spawn /usr/lib/codex/vendor/codex ENOENT' >&2; exit 1 ;; + quota) echo "ERROR: You've hit your usage limit. Try again at Oct 10th, 2026 2:55 AM." >&2; exit 1 ;; + ratelimit) echo 'ERROR: {"type":"error","status":429,"error":{"type":"rate_limit_exceeded"}}' >&2; exit 1 ;; + transient) echo 'stream error: network unreachable' >&2; exit 7 ;; +esac +`; + +interface Fixture { home: string; env: Record; argsLog: string; state: string } + +function fixture(config = 'model = "gpt-test-model"\n', auth = true): Fixture { + const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'codex-probe-sub-'))); + const stubDir = path.join(home, 'stub-bin'); + const codexHome = path.join(home, '.codex'); + const state = path.join(home, '.gstack'); + fs.mkdirSync(stubDir); + fs.mkdirSync(codexHome); + fs.mkdirSync(state); + fs.writeFileSync(path.join(stubDir, 'codex'), STUB, { mode: 0o755 }); + fs.writeFileSync(path.join(codexHome, 'config.toml'), config); + if (auth) fs.writeFileSync(path.join(codexHome, 'auth.json'), '{}'); + const argsLog = path.join(home, 'args.log'); + const env = { + PATH: [stubDir, path.dirname(process.execPath), process.env.PATH ?? ''].join(path.delimiter), + HOME: home, + CODEX_HOME: codexHome, + GSTACK_HOME: state, + STUB_ARGS_LOG: argsLog, + }; + return { home, env, argsLog, state }; +} + +function sh(shell: string, script: string, env: Record, input?: string) { + const args = shell === 'zsh' ? ['-f', '-c', script] : ['--noprofile', '--norc', '-c', script]; + const r = spawnSync(shell, args, { env, input, encoding: 'utf8', timeout: 20_000 }); + return { code: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' }; +} + +function probe(f: Fixture, args: string, extra: Record = {}, shell = 'bash', input?: string) { + return sh(shell, `'${PROBE}' ${args}`, { ...f.env, ...extra }, input); +} + +const calls = (f: Fixture) => (fs.existsSync(f.argsLog) ? fs.readFileSync(f.argsLog, 'utf8').trim().split('\n') : []); +const skipZsh = !HAS_ZSH && !ZSH_REQUIRED; + +describe('gstack-codex-probe subcommands', () => { + test('help lists every subcommand; unknown or missing subcommands print usage on stderr and exit 64', () => { + const f = fixture(); + try { + const help = probe(f, 'help'); + expect(help.code).toBe(0); + for (const name of SUBCOMMANDS) expect(help.stdout).toContain(` ${name}`); + for (const bad of ['', 'select_model', 'auth', 'select-model bogus', 'select-model exec --model', 'probe-model exec extra', + 'run-with-timeout', 'run-with-timeout 5', 'run-with-timeout soon echo hi', 'show-sandbox extra']) { + const r = probe(f, bad); + expect({ bad, code: r.code, stdout: r.stdout, usage: r.stderr.startsWith('Usage: gstack-codex-probe') }) + .toEqual({ bad, code: 64, stdout: '', usage: true }); + } + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + + for (const shell of SHELLS) { + test.skipIf(shell === 'zsh' && skipZsh)(`each subcommand runs once as a command from ${shell}, with its KEY: value contract and no deprecation line`, () => { + const f = fixture(); + try { + const results: Record> = { + 'select-model': probe(f, 'select-model exec', {}, shell), + 'check-auth': probe(f, 'check-auth', {}, shell), + 'show-sandbox': probe(f, 'show-sandbox', {}, shell), + 'check-sandbox': probe(f, 'check-sandbox', {}, shell), + 'probe-model': probe(f, 'probe-model exec', {}, shell), + 'check-version': probe(f, 'check-version', {}, shell), + 'show-first-use-notice': probe(f, 'show-first-use-notice', {}, shell), + 'run-with-timeout': probe(f, 'run-with-timeout 5 echo ran', {}, shell), + 'log-event': probe(f, 'log-event codex_timeout 540', {}, shell), + 'log-hang': probe(f, 'log-hang review 0', {}, shell), + help: probe(f, 'help', {}, shell), + }; + expect(Object.keys(results).sort()).toEqual([...SUBCOMMANDS].sort()); + for (const [name, r] of Object.entries(results)) { + expect({ name, code: r.code, deprecated: r.stderr.includes('deprecated') }).toEqual({ name, code: 0, deprecated: false }); + } + expect(results['select-model']!.stdout).toBe('CODEX_SEL: gpt-test-model\nCODEX_SEL_KIND: exec\nCODEX_SANDBOX: read-only\n'); + expect(results['select-model']!.stderr).toContain('CODEX_MODEL: gpt-test-model (exec; source: '); + expect(results['check-auth']!.stdout).toBe('AUTH_OK\n'); + expect(results['show-sandbox']!.stdout).toBe('CODEX_SANDBOX: read-only\n'); + expect(results['check-sandbox']!.stdout).toBe(''); + expect(results['probe-model']!.stdout).toBe('MODEL_OK\nCODEX_PROBE_STATE: ok\n'); + expect(results['check-version']!.stdout).toBe(''); + expect(results['show-first-use-notice']!.stdout).toBe(''); + expect(results['show-first-use-notice']!.stderr).toContain('NOTICE: gstack outside reviews send the review prompt and code to Codex'); + expect(results['run-with-timeout']!.stdout).toBe('ran\n'); + expect(results['log-event']!.stdout + results['log-event']!.stderr).toBe(''); + expect(results['log-hang']!.stdout + results['log-hang']!.stderr).toBe(''); + expect(calls(f)).toHaveLength(1); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + } + + test('nothing crosses into the caller: the selection lives only in the printed lines', () => { + const f = fixture(); + try { + const r = sh('bash', `set -u; '${PROBE}' select-model exec >/dev/null 2>&1; echo "sel=\${_GSTACK_CODEX_SEL-unset} sandbox=\${_GSTACK_CODEX_SANDBOX-unset} bin=\${_GSTACK_CODEX_BIN-unset}"`, f.env); + expect(r.stdout).toBe('sel=unset sandbox=unset bin=unset\n'); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + + test('select-model keeps the precedence: --model, then GSTACK_CODEX_MODEL, then config.toml (review_model for review)', () => { + const f = fixture('model = "gpt-5.6-terra"\nreview_model = "gpt-5.6-luna"\n'); + try { + const sel = (args: string, extra: Record = {}) => probe(f, `select-model ${args}`, extra).stdout.split('\n')[0]; + expect(sel('exec')).toBe('CODEX_SEL: gpt-5.6-terra'); + expect(sel('review')).toBe('CODEX_SEL: gpt-5.6-luna'); + expect(sel('review', { GSTACK_CODEX_MODEL: 'gpt-6-sol' })).toBe('CODEX_SEL: gpt-6-sol'); + const explicit = probe(f, "select-model exec --model 'gpt-6-luna'", { GSTACK_CODEX_MODEL: 'gpt-6-sol' }); + expect(explicit.stdout.split('\n')[0]).toBe('CODEX_SEL: gpt-6-luna'); + expect(explicit.stderr).toContain('CODEX_MODEL: gpt-6-luna (exec; source: explicit request)'); + const invalid = probe(f, "select-model exec --model 'not a model'"); + expect([invalid.code, invalid.stdout]).toEqual([1, '']); + expect(invalid.stderr).toContain('CODEX_MODEL: invalid'); + expect(calls(f)).toEqual([]); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + + test('probe-model reports CODEX_PROBE_STATE with the old exit codes, probing exactly the requested model', () => { + const f = fixture(); + try { + const cases: Array<[string, number, string]> = [ + ['model400', 1, 'unusable'], ['broken', 2, 'broken_install'], ['quota', 4, 'quota_exhausted'], + ['ratelimit', 0, 'rate_limited'], ['transient', 0, 'inconclusive'], ['ok', 0, 'ok'], + ]; + for (const [mode, code, state] of cases) { + fs.rmSync(path.join(f.state, '.codex-model-probe'), { force: true }); + const r = probe(f, 'probe-model exec', { STUB_MODE: mode }); + expect({ mode, code: r.code, state: r.stdout.match(/^CODEX_PROBE_STATE: (.*)$/m)?.[1] }).toEqual({ mode, code, state }); + } + fs.rmSync(path.join(f.state, '.codex-model-probe'), { force: true }); + fs.rmSync(f.argsLog, { force: true }); + const named = probe(f, "probe-model review --model 'gpt-6-luna'"); + expect(named.code).toBe(0); + expect(calls(f)).toHaveLength(1); + expect(calls(f)[0]).toContain('model="gpt-6-luna"'); + const invalid = probe(f, "probe-model exec --model 'not a model'"); + expect(invalid.code).toBe(1); + expect(invalid.stdout).toContain('MODEL_UNUSABLE\n'); + expect(invalid.stdout).toContain('CODEX_PROBE_STATE: unusable\n'); + expect(calls(f)).toHaveLength(1); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + + test('check-sandbox and check-auth report by exit code, reasons on stderr', () => { + const f = fixture('model = "gpt-test-model"\n', false); + try { + if (process.platform === 'linux') { + const sandbox = probe(f, 'check-sandbox', { STUB_SANDBOX: 'userns' }); + expect([sandbox.code, sandbox.stdout]).toEqual([3, '']); + expect(sandbox.stderr).toContain("Codex outside review unavailable: Codex's sandbox could not start here (bwrap: No permissions"); + } + const auth = probe(f, 'check-auth'); + expect([auth.code, auth.stdout]).toEqual([1, 'AUTH_FAILED\n']); + expect(probe(f, 'check-auth', { CODEX_API_KEY: 'present' }).code).toBe(0); + const version = probe(f, 'check-version', { STUB_VERSION: '0.120.1' }); + expect([version.code, version.stdout]).toEqual([0, '']); + expect(version.stderr).toContain('WARN: Codex CLI codex-cli 0.120.1 has known stdin deadlock bugs'); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + + for (const native of [false, true]) { + test(`run-with-timeout supervises with ${native ? 'the bash watchdog' : 'timeout(1)'}: stdin passes through, a deadline exits 124`, () => { + const f = fixture(); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-probe-sub-path-')); + try { + for (const tool of native ? ['bash', 'sleep', 'cat'] : []) { + const target = spawnSync('bash', ['-c', `command -v ${tool}`], { encoding: 'utf8', timeout: 5000 }).stdout.trim(); + if (target.startsWith('/')) fs.symlinkSync(target, path.join(dir, tool)); + } + const bash = spawnSync('bash', ['-c', 'command -v bash'], { encoding: 'utf8', timeout: 5000 }).stdout.trim(); + const env = native ? { ...f.env, PATH: dir } : f.env; + const run = (args: string[], input?: string) => { + const r = spawnSync(bash, [PROBE, 'run-with-timeout', ...args], { env, input, encoding: 'utf8', timeout: 20_000 }); + return { code: r.status, stdout: r.stdout ?? '' }; + }; + expect(run(['5', 'cat'], 'prompt on stdin')).toEqual({ code: 0, stdout: 'prompt on stdin' }); + const started = Date.now(); + expect(run(['1', 'sleep', '30']).code).toBe(124); + expect(Date.now() - started).toBeLessThan(15_000); + expect(run(['5', 'bash', '-c', 'exit 7']).code).toBe(7); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + fs.rmSync(f.home, { recursive: true, force: true }); + } + }); + } + + test('log-event follows telemetry: the exported _TEL first, else gstack-config; log-hang never prints', () => { + const f = fixture(); + try { + const usage = path.join(f.state, 'analytics', 'skill-usage.jsonl'); + const lines = () => (fs.existsSync(usage) ? fs.readFileSync(usage, 'utf8').trim().split('\n') : []); + expect(probe(f, 'log-event codex_timeout 540').code).toBe(0); + expect(lines()).toEqual([]); + fs.writeFileSync(path.join(f.state, 'config.yaml'), 'telemetry: community\n'); + expect(probe(f, 'log-event codex_timeout 540').code).toBe(0); + expect(lines()).toHaveLength(1); + expect(JSON.parse(lines()[0]!)).toMatchObject({ skill: 'codex', event: 'codex_timeout', duration_s: '540' }); + expect(probe(f, 'log-event codex_auth_failed', { _TEL: 'off' }).code).toBe(0); + expect(lines()).toHaveLength(1); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + + for (const shell of SHELLS) { + test.skipIf(shell === 'zsh' && skipZsh)(`sourcing from ${shell} still defines the functions and prints the deprecation line once`, () => { + const f = fixture(); + try { + const r = sh(shell, `. '${PROBE}' && _gstack_codex_select_model exec && echo "sel=$_GSTACK_CODEX_SEL"`, f.env); + expect(r.code).toBe(0); + expect(r.stdout).toBe('sel=gpt-test-model\n'); + expect(r.stderr.startsWith(PROBE_SOURCING_DEPRECATION)).toBe(true); + expect(r.stderr.split('deprecated').length - 1).toBe(1); + expect(PROBE_SOURCING_DEPRECATION).toContain('/gstack-upgrade'); + } finally { fs.rmSync(f.home, { recursive: true, force: true }); } + }); + } +}); + +describe('generated skills run the probe as a command', () => { + function walk(dir: string, out: string[] = []): string[] { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.') || entry.name === 'test') continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, out); + else if (/(?:SKILL\.md|\.md\.tmpl|\.md)$/.test(entry.name) || (dir.endsWith(path.join('scripts', 'resolvers')) && entry.name.endsWith('.ts'))) out.push(full); + } + return out; + } + + test('no template, resolver or generated skill sources the probe or uses its sourced-only names', () => { + const offenders: string[] = []; + const files = [...walk(path.join(ROOT, 'scripts', 'resolvers')), ...fs.readdirSync(ROOT, { withFileTypes: true }) + .filter(e => e.isDirectory() && !e.name.startsWith('.') && !['node_modules', 'test', 'docs', 'scripts'].includes(e.name)) + .flatMap(e => walk(path.join(ROOT, e.name)))]; + expect(files.length).toBeGreaterThan(100); + for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + if (/(?:^|[;&|({]|then\s)[\t ]*(?:source|\.)\s+"?[^"\s;)]*gstack-codex-probe|_gstack_codex_|_GSTACK_CODEX_/m.test(text)) offenders.push(path.relative(ROOT, file)); + } + expect(offenders).toEqual([]); + }); +}); diff --git a/test/codex-skill-isolation.test.ts b/test/codex-skill-isolation.test.ts index 2ab1aa563..5b66651bf 100644 --- a/test/codex-skill-isolation.test.ts +++ b/test/codex-skill-isolation.test.ts @@ -43,7 +43,7 @@ describe('Codex skill isolation (#2847)', () => { const calls: string[] = []; for (const file of renderedSkillFiles()) { for (const line of fs.readFileSync(file, 'utf8').split('\n')) { - if (/_gstack_codex_timeout_wrapper\s+\d+\s+codex\s+(exec|review)\b/.test(line)) calls.push(`${path.relative(ROOT, file)}: ${line}`); + if (/run-with-timeout\s+\d+\s+codex\s+(exec|review)\b/.test(line)) calls.push(`${path.relative(ROOT, file)}: ${line}`); } } expect(calls.length).toBeGreaterThan(10); diff --git a/test/codex-web-search-flag.test.ts b/test/codex-web-search-flag.test.ts index 6084dfa27..6baa5bac7 100644 --- a/test/codex-web-search-flag.test.ts +++ b/test/codex-web-search-flag.test.ts @@ -15,7 +15,7 @@ import { execFileSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'node:os'; -import { CODEX_MODEL_CONFIG_FLAG, CODEX_REVIEW_MODEL_CONFIG_FLAG, CODEX_WEB_SEARCH_FLAG } from '../scripts/resolvers/constants'; +import { CODEX_MODEL_CONFIG_FLAG, CODEX_REVIEW_MODEL_CONFIG_FLAG, CODEX_WEB_SEARCH_FLAG, codexSelect } from '../scripts/resolvers/constants'; const ROOT = path.join(import.meta.dir, '..'); const DEPRECATED = '--enable web_search_cached'; @@ -92,11 +92,12 @@ describe('deprecated codex web-search flag is gone (#2525)', () => { describe('codex frontier model flag is present', () => { // #2914: the flag carries the runtime selection, never a hard-coded default. - const flagArgv = (flag: string, select: string, env: Record, config?: string) => { + // The generated selection block, run exactly as skills run it: the executed probe, no sourcing. + const flagArgv = (flag: string, kind: 'exec' | 'review', env: Record, config?: string) => { const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-flag-model-')); try { if (config !== undefined) fs.writeFileSync(path.join(codexHome, 'config.toml'), config); - return execFileSync('bash', ['-c', `source "${path.join(ROOT, 'bin', 'gstack-codex-probe')}" && ${select} 2>/dev/null && printf '%s\\n' ${flag}`], { + return execFileSync('bash', ['-c', `exec 2>/dev/null\n${codexSelect(kind, `'${path.join(ROOT, 'bin', 'gstack-codex-probe')}'`)}\nprintf '%s\\n' ${flag}`], { env: { ...process.env, GSTACK_CODEX_MODEL: '', CODEX_HOME: codexHome, ...env }, encoding: 'utf8', timeout: 10000, }).trim().split('\n'); } finally { @@ -105,7 +106,7 @@ describe('codex frontier model flag is present', () => { }; test('the model flag passes the selected model: GSTACK_CODEX_MODEL, then config.toml, then gpt-6-astra', () => { - const select = '_gstack_codex_select_model exec'; + const select = 'exec' as const; const isolation = ['-c', 'skills.include_instructions=false']; expect(flagArgv(CODEX_MODEL_CONFIG_FLAG, select, {})).toEqual(['-c', 'model="gpt-6-astra"', ...isolation]); expect(flagArgv(CODEX_MODEL_CONFIG_FLAG, select, {}, 'model = "gpt-5.6-terra"\n')).toEqual(['-c', 'model="gpt-5.6-terra"', ...isolation]); @@ -120,7 +121,7 @@ describe('codex frontier model flag is present', () => { [{ GSTACK_CODEX_MODEL: 'custom-codex' }, 'review_model = "gpt-5.6-luna"\n', 'custom-codex'], [{}, 'model = "gpt-5.6-terra"\nreview_model = "gpt-5.6-luna"\n', 'gpt-5.6-luna'], ] as const) { - const argv = flagArgv(CODEX_REVIEW_MODEL_CONFIG_FLAG, '_gstack_codex_select_model review', { ...env }, config); + const argv = flagArgv(CODEX_REVIEW_MODEL_CONFIG_FLAG, 'review', { ...env }, config); expect(argv).toEqual(['-c', `review_model="${expected}"`, '-c', `model="${expected}"`, '-c', 'skills.include_instructions=false']); } for (const file of ['codex/sections/review-mode.md', 'review/sections/adversarial.md', 'ship/sections/adversarial.md']) { diff --git a/test/gate-tail-lint.test.ts b/test/gate-tail-lint.test.ts index 3a966a206..6a4da8b8b 100644 --- a/test/gate-tail-lint.test.ts +++ b/test/gate-tail-lint.test.ts @@ -14,7 +14,9 @@ import * as path from 'node:path'; const ROOT = path.resolve(import.meta.dir, '..'); const QUERY_SCRIPTS = ['bin/gstack-learnings-search', 'bin/gstack-timeline-read']; const GATE_CALLS = ['outside-review-result.ts', 'gstack-learnings-search', 'gstack-timeline-read', - '_gstack_codex_auth_probe', '_gstack_codex_model_probe', '_gstack_codex_sandbox_preflight']; + '_gstack_codex_auth_probe', '_gstack_codex_model_probe', '_gstack_codex_sandbox_preflight', + 'gstack-codex-probe check-auth', 'gstack-codex-probe probe-model', 'gstack-codex-probe check-sandbox', + '"$_CODEX_PROBE" check-auth', '"$_CODEX_PROBE" probe-model', '"$_CODEX_PROBE" check-sandbox']; const SILENT_EXIT = /2>\/dev\/null \|\| exit 0\b/; function walk(dir: string, keep: (file: string) => boolean, out: string[] = []): string[] { diff --git a/test/helpers/autoplan-dual-voice-evidence.ts b/test/helpers/autoplan-dual-voice-evidence.ts index 3a8dd5363..6176eebf5 100644 --- a/test/helpers/autoplan-dual-voice-evidence.ts +++ b/test/helpers/autoplan-dual-voice-evidence.ts @@ -34,7 +34,7 @@ function block(source: string, needle: string): string { } export function loadAutoplanDualCommandContract(root: string) { return { probe: block(readFileSync(join(root, 'autoplan/SKILL.md'), 'utf8'), 'echo "CODEX_MODE: $_CODEX_MODE"'), - outside: block(readFileSync(join(root, 'autoplan/sections/ceo-phase.md'), 'utf8'), '_gstack_codex_timeout_wrapper 540 codex exec') }; + outside: block(readFileSync(join(root, 'autoplan/sections/ceo-phase.md'), 'utf8'), 'run-with-timeout 540 codex exec') }; } export interface AutoplanDualEvidenceOptions { ownedRoots: string[]; diff --git a/test/helpers/carve-guards.ts b/test/helpers/carve-guards.ts index 92d6b8305..4a5cad011 100644 --- a/test/helpers/carve-guards.ts +++ b/test/helpers/carve-guards.ts @@ -166,7 +166,7 @@ export const CARVE_GUARDS: Record = { // wave's headline capability) grows the union to 1.195x. Deliberate: // the section is on-demand (loads only for Apple store targets), so // per-invocation cost for non-iOS ships is one manifest line. - maxSizeRatio: 1.4975, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + maxSizeRatio: 1.4995, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). }, 'plan-ceo-review': { skill: 'plan-ceo-review', @@ -221,7 +221,7 @@ export const CARVE_GUARDS: Record = { // 1.08 → 1.10: the scope-gate exceptions block (+ its adversarial-review // hardening: host-anchored mode signal, precedence, passing-mention // guards) and the plan-mode preamble reword land the union at 1.092. - maxSizeRatio: 1.185, // + clarity rules for saved decisions/setup gates + the Aside probe's failure reason; measured 1.1504. + test value bar and Tests to Retire in the lazy Test review section (~2.6KB); measured 1.168 + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.173 (2026-09-30). + v1.91.12.0 merge of #2999 (headless rule: a disallowed question tool never qualifies) with #3002; measured 1.1741 (2026-10-01). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 146,905 / 124,597 = 1.1790 (2026-10-06). Combined Oct 6 wave head (A and B): measured 147,524 / 124,597 = 1.1840 (2026-10-06). + maxSizeRatio: 1.1855, // + clarity rules for saved decisions/setup gates + the Aside probe's failure reason; measured 1.1504. + test value bar and Tests to Retire in the lazy Test review section (~2.6KB); measured 1.168 + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.173 (2026-09-30). + v1.91.12.0 merge of #2999 (headless rule: a disallowed question tool never qualifies) with #3002; measured 1.1741 (2026-10-01). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 146,905 / 124,597 = 1.1790 (2026-10-06). Combined Oct 6 wave head (A and B): measured 147,524 / 124,597 = 1.1840 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 147,693 / 124,597 = 1.1854 (2026-10-07). }, 'plan-design-review': { skill: 'plan-design-review', @@ -243,7 +243,7 @@ export const CARVE_GUARDS: Record = { // tier-2+ skeleton (measured 89,184). Main's v1.64.0.0 adds ~340 B more // (telemetry --error-message/--failed-step preamble prose, PR #769). // Budget covers the sum of both waves. - maxSkeletonBytes: 84_700, // Harness-aware outside voice: validated dispatch and provenance. + v1.91.19.0 wave: design --version probe (B4) and verdict-form outside voice (INV-1) on top of v1.91.17.0's round accounting (#3022); measured 80,443 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); skeleton measured 84,669 (2026-10-06). + maxSkeletonBytes: 84_850, // Harness-aware outside voice: validated dispatch and provenance. + v1.91.19.0 wave: design --version probe (B4) and verdict-form outside voice (INV-1) on top of v1.91.17.0's round accounting (#3022); measured 80,443 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); skeleton measured 84,669 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; skeleton measured 84,804 (2026-10-07). minUnionBytes: 99_200, // token-reduction Phases 1-2 (v1.69.x branch); measured union 110,293 mustContain: ['design', 'visual'], maxSizeRatio: 1.12, // D1 1.104 + main's ~0.008 @@ -348,10 +348,10 @@ do not launch the downstream skill or open a browser.`, // the #538 opt-out + D1 evidence directive — ratio 1.104 measured. // #2499 project-scope MCP jq in the brain-sync block grew every tier-2+ // skeleton ~1.5KB (entry resolution emitted once per SKILL.md). - maxSkeletonBytes: 92_600, // Office-hours + sketch outside voices include host guards and completion checks. + v1.91.19.0 wave: Q open-question prose form (#2729, approved Q3), DESIGN_READY --version probe (B4), Aside ~/.local/bin fallback (E7), verdict-form outside voice (INV-1), plus v1.91.17.0's round accounting (#3022) merged in; measured 89,684 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); skeleton measured 92,590 (2026-10-06). + maxSkeletonBytes: 92_900, // Office-hours + sketch outside voices include host guards and completion checks. + v1.91.19.0 wave: Q open-question prose form (#2729, approved Q3), DESIGN_READY --version probe (B4), Aside ~/.local/bin fallback (E7), verdict-form outside voice (INV-1), plus v1.91.17.0's round accounting (#3022) merged in; measured 89,684 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); skeleton measured 92,590 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; skeleton measured 92,869 (2026-10-07). minUnionBytes: 115_800, // Phase 4 wave 4; measured union 118,175 mustContain: ['design doc', 'problem statement'], - maxSizeRatio: 1.143, // + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 149,758 / 131,110 = 1.1422 (2026-10-06). + maxSizeRatio: 1.1445, // + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 149,758 / 131,110 = 1.1422 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 150,037 / 131,110 = 1.1444 (2026-10-07). }, 'document-release': { skill: 'document-release', @@ -411,7 +411,7 @@ do not launch the downstream skill or open a browser.`, // eureka log, the office-hours lookup and the taste-profile read; measured // 1.0834 (2026-09-30). + named design-direction defaults to avoid in the native // subagent prompt (s02 H4a); measured 1.0911 (2026-10-02). - maxSizeRatio: 1.144, // + v1.91.19.0 wave: design --version probe (B4), taste-profile load (F3), verdict-form outside voice (INV-1), plus v1.91.17.0's round accounting and board/approval blocks (#3022) merged in; measured 100,446 / 90,375 = 1.1114 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 103,384 / 90,375 = 1.1439 (2026-10-06). + maxSizeRatio: 1.1456, // + v1.91.19.0 wave: design --version probe (B4), taste-profile load (F3), verdict-form outside voice (INV-1), plus v1.91.17.0's round accounting and board/approval blocks (#3022) merged in; measured 100,446 / 90,375 = 1.1114 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 103,384 / 90,375 = 1.1439 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 103,522 / 90,375 = 1.1455 (2026-10-07). }, cso: { skill: 'cso', @@ -485,7 +485,7 @@ do not launch the downstream skill or open a browser.`, maxSkeletonBytes: 74_881, // Shared-code identity/skip/action rules + critical-severity validation; measured 74,493 (2026-09-17). + v1.91.12.0 merge of #2999 (review clarity repairs: await reads, research alongside dispatch, /review deadline and setup authority, findings sources) with #3002 (guarded state-root lines, plan-check checkpoints); each fit alone; measured 74,881 (2026-10-01). minUnionBytes: 89_000, // Phase 4 wave 1; measured union 93,357 mustContain: ['confidence', 'P1', 'P2', 'Review Army', 'adversarial'], - maxSizeRatio: 1.217, // Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + maxSizeRatio: 1.2183, // Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). }, codex: { skill: 'codex', @@ -575,7 +575,7 @@ do not launch the downstream skill or open a browser.`, maxSkeletonBytes: 70_000, // Phase-specific outside coverage, native fallback, and harness guard. minUnionBytes: 85_000, // measured union 86,926 mustContain: ['6 Decision Principles', 'TASTE DECISION', 'USER CHALLENGE', 'consensus', 'Restore Point'], - maxSizeRatio: 1.12, // Four validated outside invocations replace raw CLI calls; phases keep independent coverage. + maxSizeRatio: 1.121, // Four validated outside invocations replace raw CLI calls; phases keep independent coverage. + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 114,297 / 101,979 = 1.1208 (2026-10-07). }, spec: { skill: 'spec', diff --git a/test/helpers/codex-probe-sourcing.ts b/test/helpers/codex-probe-sourcing.ts new file mode 100644 index 000000000..21de84cc9 --- /dev/null +++ b/test/helpers/codex-probe-sourcing.ts @@ -0,0 +1,15 @@ +/** + * Sourcing bin/gstack-codex-probe is the compatibility form for skills rendered + * before the executed subcommands: it still works and prints exactly this line + * on stderr, first. Function-level tests that source the probe strip it with + * withoutDeprecation(), which fails when the line is missing or moved. + */ +export const PROBE_SOURCING_DEPRECATION = + 'gstack: sourcing gstack-codex-probe is deprecated and stops working in a release on or after 2026-10-21. Run /gstack-upgrade so your skills run it as a command (gstack-codex-probe help).\n'; + +export function withoutDeprecation(stderr: string): string { + if (!stderr.startsWith(PROBE_SOURCING_DEPRECATION)) { + throw new Error(`sourcing gstack-codex-probe did not print its deprecation line first; stderr was:\n${stderr}`); + } + return stderr.slice(PROBE_SOURCING_DEPRECATION.length); +} diff --git a/test/outside-voice-invocation.test.ts b/test/outside-voice-invocation.test.ts index 1eef30973..08d7589fb 100644 --- a/test/outside-voice-invocation.test.ts +++ b/test/outside-voice-invocation.test.ts @@ -332,13 +332,15 @@ describe('generated outside-review dispatch', () => { test('autoplan retains its Codex timeout event and hang record', () => { const events = path.join(TMP, 'autoplan-events'); const probe = path.join(BIN, 'gstack-codex-probe'); - fs.writeFileSync(probe, `_gstack_codex_select_model() { _GSTACK_CODEX_SEL=gpt-6-astra; _GSTACK_CODEX_SANDBOX=read-only; } -_gstack_codex_sandbox_preflight() { return 0; } -_gstack_codex_first_use_notice() { :; } -_gstack_codex_timeout_wrapper() { echo 'Partial finding'; return 124; } -_gstack_codex_log_event() { printf '%s %s\\n' "$1" "$2" >> "$FAKE_EVENTS"; } -_gstack_codex_log_hang() { printf '%s %s\\n' "$1" "$2" >> "$FAKE_EVENTS"; } -`); + fs.writeFileSync(probe, `#!/usr/bin/env bash +case "$1" in + select-model) printf 'CODEX_SEL: gpt-6-astra\\nCODEX_SEL_KIND: exec\\nCODEX_SANDBOX: read-only\\n' ;; + check-sandbox|show-first-use-notice) ;; + run-with-timeout) echo 'Partial finding'; exit 124 ;; + log-event|log-hang) printf '%s %s\\n' "$2" "$3" >> "$FAKE_EVENTS" ;; + *) exit 64 ;; +esac +`, { mode: 0o755 }); const ctx: TemplateContext = { skillName: 'autoplan', tmplPath: 'autoplan/SKILL.md.tmpl', host: 'claude', paths: { ...HOST_PATHS.claude, binDir: BIN, skillRoot: ROOT } }; const command = outsideVoiceCommand(ctx, { promptFile: PROMPT, timeoutMs: 600000 }); @@ -416,7 +418,7 @@ echo 'Recommendation: approve because the late answer arrived.' const rendered = outsideVoiceInvocation(ctx, { timeoutMs: requested }); const gates = [...rendered.matchAll(/timeout: (\d+)/g)].map(m => Number(m[1])); expect(gates).toEqual([provider + 60000]); - expect(rendered).toContain(host === 'claude' ? `_gstack_codex_timeout_wrapper ${provider / 1000} codex` : `--timeout-ms ${provider}`); + expect(rendered).toContain(host === 'claude' ? `run-with-timeout ${provider / 1000} codex` : `--timeout-ms ${provider}`); } } }); diff --git a/test/regression-issue3024-zsh-sourced-helpers.test.ts b/test/regression-issue3024-zsh-sourced-helpers.test.ts index f0f49c51e..9fcfd1d44 100644 --- a/test/regression-issue3024-zsh-sourced-helpers.test.ts +++ b/test/regression-issue3024-zsh-sourced-helpers.test.ts @@ -22,6 +22,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { codexPreflight } from '../scripts/resolvers/constants'; +import { PROBE_SOURCING_DEPRECATION, withoutDeprecation } from './helpers/codex-probe-sourcing'; const ROOT = path.resolve(import.meta.dir, '..'); const BIN = path.join(ROOT, 'bin'); @@ -52,6 +53,9 @@ const code = (text: string) => text.split('\n').filter(line => !/^\s*#/.test(lin * Every bin/ helper a generated skill or resolver loads with `source` or `.`, * plus every bin/ file those helpers source in turn (for example * gstack-state-root.sh, sourced by the codex probe and the egress lib). + * Current skills run gstack-codex-probe as a command, but skills rendered + * before that change still source it until the compatibility window closes, + * so it stays on the list until sourcing is removed. */ function sourcedHelpers(): string[] { const files = [ @@ -60,7 +64,7 @@ function sourcedHelpers(): string[] { .filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'test') .flatMap(e => walk(path.join(ROOT, e.name), f => /(?:SKILL\.md|\.md\.tmpl)$/.test(f))), ]; - const names = new Set(); + const names = new Set(['gstack-codex-probe']); const direct = /(?:^|[\s;&|({]|then\s)(?:source|\.)\s+"?[^"\s;)<]*\bbin\/([A-Za-z0-9._-]+)/g; const viaVar = /\/(gstack-[A-Za-z0-9._-]+\.sh)"; \[ -r "\$[A-Z_]+" \] && \. "\$[A-Z_]+"/g; const nested = /(?:^|[\s;&|({]|then\s)(?:source|\.)\s+"?\$\{?[A-Za-z_][A-Za-z0-9_]*\}?\/([A-Za-z0-9._-]+)/g; @@ -167,7 +171,8 @@ describe('#3024: sourced helpers work under bash and zsh', () => { for (const name of helpers) { const file = path.join(BIN, name); const r = run(shell, `${shell} -n '${file}' && . '${file}' && echo SOURCED`); - expect({ name, out: r.out.trim().split('\n').pop() }).toEqual({ name, out: 'SOURCED' }); + const deprecation = name === 'gstack-codex-probe' ? PROBE_SOURCING_DEPRECATION : ''; + expect({ name, stdout: r.stdout, stderr: r.stderr }).toEqual({ name, stdout: 'SOURCED\n', stderr: deprecation }); } }); @@ -219,7 +224,8 @@ describe('#3024: sourced helpers work under bash and zsh', () => { LOCAL_PROVIDER_KEY: 'present', }); fs.rmSync(codexHome, { recursive: true, force: true }); - expect(r.out.trim()).toBe('AUTH_OK'); + expect(r.stdout.trim()).toBe('AUTH_OK'); + expect(withoutDeprecation(r.stderr)).toBe(''); expect(r.code).toBe(0); }); @@ -259,17 +265,20 @@ describe('#3024: sourced helpers work under bash and zsh', () => { expect(r.out).toContain('rc=1 bin=unset'); }); - for (const [label, shell, prefix, expected] of [ - ['dash', 'dash', '', `gstack: cannot locate gstack-codex-probe (shell: dash). Source it from bash or zsh, or export GSTACK_ROOT=. ${ANCHOR}`], - ['an unknown shell', 'bash', 'unset BASH_VERSION\n', `gstack: cannot locate gstack-codex-probe (shell: bash). Source it from bash or zsh, or export GSTACK_ROOT=. ${ANCHOR}`], + // The preflight runs the probe as a command, so the calling shell no longer + // matters: dash and a shell without BASH_VERSION reach the same verdict. + for (const [label, shell, prefix] of [ + ['dash', 'dash', ''], + ['an unknown shell', 'bash', 'unset BASH_VERSION\n'], ] as const) { - test.skipIf(shell === 'dash' && skipDash)(`the full generated preflight under ${label} reports helper_unavailable with the helper's line`, () => { + test.skipIf(shell === 'dash' && skipDash)(`the full generated preflight under ${label} runs the probe as a command and reaches CODEX_MODE: ready`, () => { const { home, env } = preflightHome(); try { const r = run(shell, `${prefix}${PREFLIGHT}`, env, home); - expect(r.out).toContain(`${expected}\nCODEX_MODE: helper_unavailable\n`); - expect(r.out).not.toContain('not_authed'); - expect(r.out).not.toMatch(/Syntax error|not found/); + expect(r.out).toContain('CODEX_MODEL: gpt-test-model (exec;'); + expect(r.out).toContain('CODEX_MODE: ready\n'); + expect(r.out).not.toContain('cannot locate'); + expect(r.out).not.toMatch(/Syntax error|not found|deprecated/); } finally { fs.rmSync(home, { recursive: true, force: true }); } }); } diff --git a/test/run-in-background-guidance.test.ts b/test/run-in-background-guidance.test.ts index 183bf3a97..0e0870b1e 100644 --- a/test/run-in-background-guidance.test.ts +++ b/test/run-in-background-guidance.test.ts @@ -258,8 +258,8 @@ describe('outside-voice dispatch contract', () => { const apply = rendered.indexOf('**4. Apply the answered row.**'); expect(answer).toBeGreaterThan(0); expect(apply).toBeGreaterThan(answer); - // B1: the sandbox comes from _gstack_codex_select_model (read-only unless GSTACK_CODEX_NO_SANDBOX=1). - expect(rendered).toContain(`-s "\${_GSTACK_CODEX_SANDBOX:?}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="high"'`); + // B1: the sandbox comes from the probe's select-model (read-only unless GSTACK_CODEX_NO_SANDBOX=1). + expect(rendered).toContain(`-s "\${_CODEX_SANDBOX_MODE:?}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="high"'`); }); test('the generated-carrier exception rejects missing wait, cancellation or result guards', () => { diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index f9bca7e5c..7e55247c7 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -1517,8 +1517,8 @@ describe('Codex skill', () => { const resumeCommand = match![0]; expect(resumeCommand).not.toContain(' -C '); expect(resumeCommand).not.toMatch(/ -s /); - // resume takes the sandbox only as config; _gstack_codex_select_model sets it (read-only by default). - expect(resumeCommand).toContain('-c "sandbox_mode=\\"${_GSTACK_CODEX_SANDBOX:?}\\""'); + // resume takes the sandbox only as config; the probe's select-model reports it (read-only by default). + expect(resumeCommand).toContain('-c "sandbox_mode=\\"${_CODEX_SANDBOX_MODE:?}\\""'); }); test('codex union contains cost tracking', () => {