feat(codex): run gstack-codex-probe as a command, one subcommand per probe

Generated skills now execute bin/gstack-codex-probe (select-model, check-auth,
show-sandbox, check-sandbox, probe-model, check-version, show-first-use-notice,
run-with-timeout, log-event, log-hang, help) and capture its KEY: value lines,
so no helper state crosses into the calling shell and the preflight works from
bash, zsh or sh. select-model keeps --model > GSTACK_CODEX_MODEL > config.toml
precedence; run-with-timeout supervises with the existing gtimeout/timeout/
watchdog chain and returns 124 on a deadline; an unknown subcommand exits 64.

Sourcing keeps working for skills rendered before the upgrade and prints one
deprecation line naming /gstack-upgrade; it and the zsh self-locate block are
removed in a release on or after 2026-10-21 (DX-18). Parity caps raised with
measured notes for the longer capture lines.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-07 19:53:35 +00:00
1 parent 7fab2a7139
commit 64e8f90f15
47 files changed
+937 -399

No files matched your search

+5 -2
View File
@@ -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[@]}"
+18 -14
View File
@@ -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.
+9 -6
View File
@@ -111,15 +111,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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
+9 -6
View File
@@ -70,15 +70,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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
+9 -6
View File
@@ -74,15 +74,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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
+9 -6
View File
@@ -63,15 +63,18 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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
+117 -7
View File
@@ -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 <exec|review> [--model <id>] 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 <exec|review> [--model <id>] 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 <secs> <cmd...> the command's exit code; 124 on timeout
# log-event <event> [detail] silent; exit 0
# log-hang <skill> <prompt-size> 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 <subcommand> [args]
select-model <exec|review> [--model <id>] 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 <exec|review> [--model <id>] 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 <secs> <cmd...> run cmd under a deadline; its exit code, 124 on timeout
log-event <event> [detail] record a telemetry event when telemetry is on; exit 0
log-hang <skill> <prompt-size> record a timeout learning; exit 0
help this text
EOF
}
# Parses "<exec|review> [--model <id>]" 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
+17 -19
View File
@@ -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: <model> (<kind>; 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 '<model>'` before the probe below. An
invalid or unavailable choice stops with a repair message, never the default.
For a named model, add `--model '<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
+16 -18
View File
@@ -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: <model> (<kind>; 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 '<model>'` before the probe below. An
invalid or unavailable choice stops with a repair message, never the default.
For a named model, add `--model '<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
+9 -7
View File
@@ -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/<prompt-file-name>"
[ -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"
+6 -7
View File
@@ -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/<prompt-file-name>"
[ -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"
+9 -7
View File
@@ -101,8 +101,10 @@ fi
cd "$_REPO_ROOT" || exit 1
PROMPT_FILE="$_REPO_ROOT/.gstack/tmp/<prompt-file-name>"
[ -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"
+6 -7
View File
@@ -92,8 +92,7 @@ fi
cd "$_REPO_ROOT" || exit 1
PROMPT_FILE="$_REPO_ROOT/.gstack/tmp/<prompt-file-name>"
[ -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"
+17 -13
View File
@@ -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 <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 <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/<focus-file-name>"
[ -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 "<base>...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"
+11 -13
View File
@@ -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 <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 <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/<focus-file-name>"
[ -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 "<base>...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"
@@ -178,11 +178,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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; }
+7 -4
View File
@@ -1678,11 +1678,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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; }
+22 -12
View File
@@ -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.
<a id="sourced-helper-location"></a>
### `gstack: cannot locate gstack-codex-probe (shell: ...)` / `CODEX_MODE: helper_unavailable`
### `gstack: cannot load gstack-codex-probe` / `gstack: cannot locate <helper> (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 <helper> (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: <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: <model>` (and `CODEX_MODEL: <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.
<a id="codex-sandbox-unavailable"></a>
### `Codex outside review unavailable: Codex's sandbox could not start here (...)`
+25 -18
View File
@@ -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 -- '<prepared-prompt-file>' >"$_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; }
+14 -8
View File
@@ -840,11 +840,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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 -- '<prepared-prompt-file>' >"$_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; }
+25 -18
View File
@@ -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 -- '<prepared-prompt-file>' >"$_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; }
+7 -4
View File
@@ -1050,11 +1050,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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; }
+25 -18
View File
@@ -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 -- '<prepared-prompt-file>' >"$_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; }
+25 -18
View File
@@ -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 -- '<prepared-prompt-file>' >"$_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; }
+32 -23
View File
@@ -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 -- '<prepared-prompt-file>' >"$_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 '<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 '<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; }
+53 -32
View File
@@ -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: <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}
+2 -1
View File
@@ -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<string, ResolverFn> = {
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,
+9 -9
View File
@@ -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<TemplateContext, 'host'>) {
@@ -106,14 +106,14 @@ export function outsideVoiceCommand(ctx: TemplateContext, opts: OutsideCommandOp
const root = toShellPath(ctx.paths.skillRoot);
const prompt = sh(opts.promptFile ?? '<prepared-prompt-file>');
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
+32 -23
View File
@@ -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 -- '<prepared-prompt-file>' >"$_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 '<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 '<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; }
+7 -4
View File
@@ -183,11 +183,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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; }
+7 -4
View File
@@ -178,11 +178,14 @@ trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
_OUTSIDE_INPUT="$_OUTSIDE_TMP/prompt"
cat -- '<prepared-prompt-file>' >"$_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; }
+2 -2
View File
@@ -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("'<prepared-prompt-file>'","'"+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';
+1 -1
View File
@@ -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 "<base>...HEAD"'));
expect(codex).toContain('--finish CODEX_REVIEW_START');
expect(codex).toContain('"completed":COMPLETED,"converged":CONVERGED');
+11 -11
View File
@@ -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=$?');
});
+8 -8
View File
@@ -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);
});
});
+2 -1
View File
@@ -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<string, string>
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 {
+280
View File
@@ -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<string, string>; 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<string, string>, 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<string, string> = {}, 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<string, ReturnType<typeof probe>> = {
'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<string, string> = {}) => 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([]);
});
});
+1 -1
View File
@@ -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);
+6 -5
View File
@@ -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<string, string>, 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<string, string>, 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']) {
+3 -1
View File
@@ -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[] {
+1 -1
View File
@@ -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[];
+8 -8
View File
@@ -166,7 +166,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
// 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<string, CarveGuard> = {
// 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<string, CarveGuard> = {
// 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<N> 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<N> 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',
+15
View File
@@ -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);
}
+10 -8
View File
@@ -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}`);
}
}
});
@@ -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<string>();
const names = new Set<string>(['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=<install dir>. ${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=<install dir>. ${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 }); }
});
}
+2 -2
View File
@@ -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', () => {
+2 -2
View File
@@ -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', () => {