From 70fab3e246126da1004abd320e728c4c07b2b5cd Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 12 Oct 2026 13:17:02 -0700 Subject: [PATCH] v1.91.94.0 feat(codex): --effort overrides reasoning effort for one run (#3146) (#3151) * feat(codex): --effort sets GSTACK_CODEX_EFFORT for one run /codex review --effort medium (or --effort=medium) prefixes each run-with-timeout dispatch with GSTACK_CODEX_EFFORT=, which outranks codex_effort and the per-mode default; --xhigh becomes shorthand for --effort xhigh. The probe keeps owning validation. A missing value, a value that is not one lowercase word, or conflicting flags stop instead of falling back. Co-Authored-By: Claude Opus 5.5 * test(codex): per-run effort prefix reaches the rendered /codex review dispatch Runs the rendered review fence against the fake codex: no prefix uses codex_effort, GSTACK_CODEX_EFFORT=medium|xhigh wins over it, an unknown level never reaches codex. Step 1 check pins --effort tokens and the stop rule. Co-Authored-By: Claude Opus 5.5 * docs(codex): document codex_effort and --effort; Challenge runs at high, not xhigh Co-Authored-By: Claude Opus 5.5 * docs(changelog): [Unreleased] entry for /codex --effort Co-Authored-By: Claude Opus 5.5 * fix(codex): --effort reaches the probe as an allowlisted request level, parsed before the mode and checked before the paid probe The level now travels as run-with-timeout --request-effort '' (placeholder grammar ^[a-z]*$) instead of an agent-built env prefix, and the probe names /codex --effort when it rejects one. Effort flags count only right after /codex or its mode word and are removed before mode detection. Step 0.5 runs check-effort before probe-model, so a bad level makes no Codex call even on a cold cache. Tests cover all five dispatch fences and the zero-call cases. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(codex): per-request --effort in model-policy, CHANGELOG behavior changes, TODOS follow-ups Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.93.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(codex): effort flags sit among /codex's leading flags; the probe names an unfilled '' placeholder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(codex): per-request row in the effort precedence table, in-tool help names /codex --effort, late-flag notice Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(codex): binding tripwire sees the request-effort dispatch; valid and unfilled levels through Step 0.5 and every fence; doc accuracy Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: John Nunemaker Co-authored-by: Claude Opus 5.5 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++ TODOS.md | 7 ++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bin/gstack-codex-probe | 48 +++++++++---- bin/gstack-config | 5 +- codex/SKILL.md | 34 +++++----- codex/SKILL.md.tmpl | 34 +++++----- codex/sections/challenge-mode.md | 2 +- codex/sections/challenge-mode.md.tmpl | 2 +- codex/sections/consult-mode.md | 4 +- codex/sections/consult-mode.md.tmpl | 4 +- codex/sections/review-mode.md | 4 +- codex/sections/review-mode.md.tmpl | 4 +- docs/model-policy.md | 17 +++-- docs/skills.md | 4 +- lib/model-policy-cli.ts | 7 +- package.json | 2 +- test/binding-template-drift.test.ts | 4 +- test/codex-effort.test.ts | 97 +++++++++++++++++++++++++-- test/codex-hardening.test.ts | 8 +-- test/codex-invocation-sites.test.ts | 2 +- test/codex-skill-isolation.test.ts | 2 +- test/helpers/placeholder-allowlist.ts | 1 + test/model-role-invocation.test.ts | 23 ++++++- test/outside-send-boundary.test.ts | 4 +- 26 files changed, 257 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df661fe0..f84d4850e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.91.94.0] - 2026-10-12 + +**`/codex review --effort medium` runs one review at the effort you name, without changing your saved setting.** + +`codex_effort` (1.91.92.0) sets Codex reasoning effort for every run, and `--xhigh` raised it for one. There was no per-run way to go lower, so a cheaper one-off review meant flipping the setting and flipping it back. + +### What this means for you + +- **`--effort ` on any `/codex` mode.** `/codex review --effort medium`, `/codex challenge --effort=low` and so on set the effort for that request only. It outranks an exported `GSTACK_CODEX_EFFORT`, `codex_effort` and the per-mode defaults. `--xhigh` keeps working as shorthand for `--effort xhigh`. +- **Flags only where flags go.** The flag counts directly after `/codex` or its mode word, alongside `-m`/`--model`/`--role`, before your prompt. `/codex what does --effort do?` sends that question to Codex instead of eating `--effort` as a setting, and `/codex review --effort medium` stays on native review. This also applies to `--xhigh`, which used to count anywhere in the input: `/codex review fix the parser --xhigh` now sends `--xhigh` as part of the instructions, so put it first (`/codex review --xhigh fix the parser`). +- **A typo costs nothing and says what to fix.** `--effort turbo` stops before any Codex call, including the model probe on a cold cache, with `'turbo' from /codex --effort is not a Codex reasoning effort level` and the list of levels. A missing value or conflicting effort flags stop with a message too. +- **docs:** `docs/model-policy.md` documents the per-request flag, and `docs/skills.md` documents `codex_effort` and `--effort` and no longer claims Challenge runs at `xhigh` (it runs at `high`). + +### For contributors + +- `gstack-codex-probe check-effort --request ` and `run-with-timeout --effort --request-effort ` take the request level (empty means none) and name `/codex --effort` as its source. The five `/codex` dispatch fences pass it as the allowlisted `''` placeholder (grammar `^[a-z]*$`), so the level reaches the shell only as that quoted, allowlisted placeholder (never through prose), and the probe accepts only the seven levels. Test parsers of the `run-with-timeout` line accept the new argument. +- `test/codex-effort.test.ts` runs all five rendered dispatches against a fake `codex`, and the bogus-level case asserts the rejection and zero calls. `test/model-role-invocation.test.ts` adds a zero-call case for an invalid level in Step 0.5. + +Contributed by @jnunemaker + ## [1.91.93.0] - 2026-10-12 **/cso release qualification gates on finished reports. Report completeness is still measured, but it no longer blocks a release.** diff --git a/TODOS.md b/TODOS.md index b2aadc566..9c7c45583 100644 --- a/TODOS.md +++ b/TODOS.md @@ -2,6 +2,13 @@ ## NEXT PRIORITY +### P3: `/codex --effort` follow-ups (filed 2026-10-11, from the #3146 autoplan review) + +- **Show the effort that ran.** Step 0.5 prints `CODEX_EFFORT: (source: /codex --effort)` once, but the result footers do not repeat the effort, so it scrolls away. Add an `Effort:` line to the review, challenge and consult result footers. Blocked on skeleton bytes in `codex/SKILL.md` (cap 60,200). **Effort:** S. **Priority:** P3. +- **Timeouts at high effort.** `xhigh`, `max` and `ultra` can exceed the fixed 330 s / 540 s wrappers, and the timeout copy blames prompt size or API speed. Name the effort in the timeout message and suggest a lower `--effort`; consider effort-scaled budgets. **Effort:** S. **Priority:** P3. +- **Per-run effort for gate voices (#2934).** `/review`, `/ship` and plan-review outside voices take `GSTACK_CODEX_EFFORT` and `codex_effort` but have no per-request flag; the Claude Code runner takes no effort at all. Needs its own precedence and consent review. **Effort:** M. **Priority:** P3. +- **`minimal` with web search.** Every `/codex` dispatch passes `web_search="cached"`, and Codex reportedly rejects that with `reasoning.effort: minimal` (openai/codex#3559, #5002). Unverified here (no Codex CLI on the release machine); the same path already ships through `codex_effort minimal`. Verify on the pinned CLI and drop web search for `minimal` if it fails. **Effort:** S. **Priority:** P3. + ### P2/P3: open bugs by area after the Wave 9 triage (filed 2026-10-11) The 158 older open issues were re-checked against main 5cb5e1c (v1.91.83.0). Of the 30 still-valid bugs, Wave 9 lanes take #915, #1080, #1081, #1150, #1315 and #1675 and park #2118 and #2623 (next section). The rest stay open here: issue, root-cause file, size. Bugs already tracked elsewhere in this file point there instead of repeating it. diff --git a/VERSION b/VERSION index 05b7fbcf8..d827e1c7a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.93.0 +1.91.94.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index d1685ba3e..25b0940ba 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.93.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.94.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index 6bf0fc6c1..a40e0df1a 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -6,7 +6,8 @@ # # Subcommands (each wraps one function below): # select-model [--model ] CODEX_SEL:, CODEX_SEL_KIND:, CODEX_SANDBOX: -# check-effort CODEX_EFFORT:; exit 0, or 1 on an invalid setting +# check-effort [--request ] +# CODEX_EFFORT:; exit 0, or 1 on an invalid setting # check-auth exit 0 authed, 1 not # show-sandbox CODEX_SANDBOX: # check-sandbox exit 0, or 3 with the reason on stderr @@ -17,11 +18,12 @@ # CODEX_PROBE_STATE:; exit 0, 1, 2, 3 or 4 # check-version warnings on stderr; exit 0 # show-first-use-notice notice on stderr; exit 0 -# run-with-timeout [--user-invoked] [--effort ] +# run-with-timeout [--user-invoked] [--effort [--request-effort ]] # the command's exit code; 124 on timeout; # 3 when codex_reviews refuses the send; # --effort appends the resolved -# model_reasoning_effort to a codex command +# model_reasoning_effort to a codex command; +# --request-effort is /codex --effort (empty: none) # log-event [detail] silent; exit 0 # log-hang silent; exit 0 # help usage; an unknown subcommand exits 64 @@ -848,15 +850,22 @@ _gstack_codex_log_hang() { # --- Reasoning effort (#3098, #2934) ------------------------------------------ _gstack_codex_effort_check() { - # One precedence, like the model: GSTACK_CODEX_EFFORT (per run), then - # codex_effort in config.yaml, then the gate's own default in $1. Codex only + # One precedence, like the model: the request's /codex --effort in $2, then + # GSTACK_CODEX_EFFORT (per run), then codex_effort in config.yaml, then the + # gate's own default in $1. Codex only # rejects an unknown model_reasoning_effort at request time, so an invalid # setting stops here, before any paid call, naming the source to fix. The # allowlist is the union of Codex's config levels and the API's minimal; # levels are model-dependent. A default outside it is a usage error (64). - local _default="$1" _chosen _source _cfg + local _default="$1" _request="${2:-}" _chosen _source _cfg case "$_default" in minimal|low|medium|high|xhigh|max|ultra) ;; *) return 64 ;; esac - if [ -n "${GSTACK_CODEX_EFFORT:-}" ]; then + if [ "$_request" = "" ]; then + echo "CODEX_EFFORT: invalid — the '' placeholder was left unfilled. Pass '' when the request has no --effort. No Codex call was made." >&2 + return 1 + fi + if [ -n "$_request" ]; then + _chosen="$_request"; _source="/codex --effort" + elif [ -n "${GSTACK_CODEX_EFFORT:-}" ]; then _chosen="$GSTACK_CODEX_EFFORT"; _source="env GSTACK_CODEX_EFFORT" else _cfg=$("$_GSTACK_CODEX_BIN/gstack-config" get codex_effort 2>/dev/null || true) @@ -867,7 +876,11 @@ _gstack_codex_effort_check() { minimal|low|medium|high|xhigh|max|ultra) ;; *) echo "CODEX_EFFORT: invalid — '$_chosen' from $_source is not a Codex reasoning effort level. No Codex call was made." >&2 - echo "HINT: use one of minimal, low, medium, high, xhigh, max or ultra (export GSTACK_CODEX_EFFORT= or gstack-config set codex_effort ), or unset it to keep each gate's own effort." >&2 + if [ -n "$_request" ]; then + echo "HINT: use one of minimal, low, medium, high, xhigh, max or ultra (e.g. /codex review --effort medium), or drop --effort to keep the configured effort. See docs/model-policy.md#reasoning-effort." >&2 + else + echo "HINT: use one of minimal, low, medium, high, xhigh, max or ultra (export GSTACK_CODEX_EFFORT= or gstack-config set codex_effort ), or unset it to keep each gate's own effort." >&2 + fi return 1 ;; esac @@ -881,8 +894,9 @@ _gstack_codex_usage() { printf '%s\n' \ 'Usage: gstack-codex-probe [args]' \ ' select-model [--model ] print CODEX_SEL:, CODEX_SEL_KIND: and CODEX_SANDBOX:; exit 0 or 1' \ - ' check-effort GSTACK_CODEX_EFFORT, else codex_effort, else the default; print' \ - ' CODEX_EFFORT:; exit 0, 1 on an invalid setting' \ + ' check-effort [--request ]' \ + ' the request level, else GSTACK_CODEX_EFFORT, else codex_effort,' \ + ' else the default; print CODEX_EFFORT:; exit 0, 1 on an invalid setting' \ ' 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' \ @@ -892,12 +906,13 @@ _gstack_codex_usage() { ' CODEX_SEL:, CODEX_SEL_KIND:, CODEX_SANDBOX:, CODEX_PROBE_STATE:' \ ' 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 [--user-invoked] [--effort ] ' \ + ' run-with-timeout [--user-invoked] [--effort [--request-effort ]] ' \ ' run cmd under a deadline once codex_reviews allows the send;' \ ' its exit code, 124 on timeout, 3 refused (consent_missing).' \ ' --user-invoked: the user asked for this review by name' \ ' --effort: append -c model_reasoning_effort= to a codex' \ ' command, as check-effort resolves it; 1 on an invalid setting' \ + ' --request-effort: /codex --effort, outranking the rest (empty: none)' \ ' log-event [detail] record a telemetry event when telemetry is on; exit 0' \ ' log-hang record a timeout learning; exit 0' \ ' help this text' @@ -948,8 +963,8 @@ _gstack_codex_main() { _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-effort) - [ "$#" -eq 1 ] || { _gstack_codex_usage >&2; return 64; } - _gstack_codex_effort_check "$1" + { [ "$#" -eq 1 ] || { [ "$#" -eq 3 ] && [ "$2" = "--request" ]; }; } || { _gstack_codex_usage >&2; return 64; } + _gstack_codex_effort_check "$1" "${3:-}" _rc=$? [ "$_rc" -eq 64 ] && { _gstack_codex_usage >&2; return 64; } return "$_rc" ;; @@ -988,10 +1003,15 @@ _gstack_codex_main() { run-with-timeout) _GSTACK_CODEX_USER_INVOKED=0 _GSTACK_CODEX_EFFORT_DEFAULT="" + _GSTACK_CODEX_EFFORT_REQUEST="" if [ "${1:-}" = "--user-invoked" ]; then _GSTACK_CODEX_USER_INVOKED=1; shift; fi if [ "${1:-}" = "--effort" ]; then { [ "$#" -ge 2 ] && [ -n "$2" ]; } || { _gstack_codex_usage >&2; return 64; } _GSTACK_CODEX_EFFORT_DEFAULT="$2"; shift 2 + if [ "${1:-}" = "--request-effort" ]; then + [ "$#" -ge 2 ] || { _gstack_codex_usage >&2; return 64; } + _GSTACK_CODEX_EFFORT_REQUEST="$2"; shift 2 + fi fi case "${1:-}" in ''|*[!0-9.]*) _gstack_codex_usage >&2; return 64 ;; esac [ "$#" -ge 2 ] || { _gstack_codex_usage >&2; return 64; } @@ -999,7 +1019,7 @@ _gstack_codex_main() { _gstack_codex_consent "$_GSTACK_CODEX_USER_INVOKED" || return 3 if [ -n "$_GSTACK_CODEX_EFFORT_DEFAULT" ]; then _rc=0 - _GSTACK_CODEX_EFFORT_LINE=$(_gstack_codex_effort_check "$_GSTACK_CODEX_EFFORT_DEFAULT") || _rc=$? + _GSTACK_CODEX_EFFORT_LINE=$(_gstack_codex_effort_check "$_GSTACK_CODEX_EFFORT_DEFAULT" "$_GSTACK_CODEX_EFFORT_REQUEST") || _rc=$? [ "$_rc" -eq 64 ] && { _gstack_codex_usage >&2; return 64; } [ "$_rc" -eq 0 ] || return "$_rc" set -- "$@" -c "model_reasoning_effort=\"${_GSTACK_CODEX_EFFORT_LINE#CODEX_EFFORT: }\"" diff --git a/bin/gstack-config b/bin/gstack-config index 059189272..53122a6be 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -185,7 +185,8 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne # codex_effort: # Codex reasoning effort for outside reviews and /codex: # # minimal | low | medium | high | xhigh | max | ultra. # # Absent = every gate keeps its effort. GSTACK_CODEX_EFFORT -# # overrides it per run; /spec keeps medium (120 s budget). +# # overrides it per run, /codex --effort per request; +# # /spec keeps medium (120 s budget). # Docs: docs/model-policy.md # # ─── Advanced ──────────────────────────────────────────────────────── @@ -335,7 +336,7 @@ lookup_default() { plan_review_tier) echo "frontier" ;; # frontier | smart | host — lib/model-policy.ts reads it strictly implementation_tier) echo "smart" ;; # frontier | smart model_frontier_claude|model_frontier_openai|model_smart_claude|model_smart_openai) echo "" ;; # empty = shipped catalog ID (gstack-models list) - codex_effort) echo "" ;; # empty = each gate's own effort; GSTACK_CODEX_EFFORT wins (gstack-codex-probe run-with-timeout --effort) + codex_effort) echo "" ;; # empty = each gate's own effort; /codex --effort, then GSTACK_CODEX_EFFORT, win (gstack-codex-probe run-with-timeout --effort) ship_greptile_early) echo "true" ;; # true | false — /ship's early Greptile PR # Unknown key: exit non-zero instead of printing "". The fallback pattern # the preambles use, diff --git a/codex/SKILL.md b/codex/SKILL.md index a2d4ef2ea..72c04a24a 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -483,10 +483,17 @@ Only for an explicit `--role plan-review` (e.g. `/codex challenge --role plan-re set `_CODEX_ROLE='plan-review'` below and in the mode block; the mode block then probes and dispatches the policy model within its own timeout. [Policy setup](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md). +**Effort flags, read before the mode:** `--effort `, `--effort=` or `--xhigh` +(= `--effort xhigh`) directly after `/codex` or its mode word, among its leading flags +(`-m`, `--model`, `--role`) and before any prompt text, set `` (else `''`); +remove them before Step 1. A later `--effort` is prompt text; if a level follows, say so. Missing +value, not one lowercase word, or differing levels: stop, name the levels; nothing is sent. + ```bash _CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_ROLE='' [ -x "$_CODEX_PROBE" ] || { echo "HELPER_UNAVAILABLE"; exit 1; } +"$_CODEX_PROBE" check-effort high --request '' >/dev/null || exit 1 # free # GSTACK_ACTIVE_HOST names the harness, never the model. if { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ] || [ "${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then @@ -507,6 +514,8 @@ fi "$_CODEX_PROBE" check-version # warns if known-bad, non-blocking ``` +If `check-effort` failed, relay its lines and stop; no Codex call was made. + 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: `gstack-codex-probe` is missing or @@ -570,7 +579,7 @@ After this, every subsequent bash block in this skill uses `"$PLAN_ROOT"` and ## Step 1: Detect mode -Parse the user's input to determine which mode to run: +Parse the input, effort flags removed, to determine which mode to run: 1. `/codex review` or `/codex review ` — **Review mode** (Step 2A) 2. `/codex challenge` or `/codex challenge ` — **Challenge mode** (Step 2B) @@ -596,11 +605,8 @@ The three modes are MUTUALLY EXCLUSIVE — at most one runs per invocation. Once the mode is determined, read ONLY that mode's section (see the Section index above); never read the other two mode sections. -**Reasoning effort override:** If the user's input contains `--xhigh` anywhere, -note it and remove it from the prompt text before passing to Codex. When `--xhigh` -is present, prefix the `run-with-timeout` dispatch with `GSTACK_CODEX_EFFORT=xhigh`, -which outranks `codex_effort` and the per-mode default. Otherwise `--effort` applies -`GSTACK_CODEX_EFFORT`, then `codex_effort`, then the per-mode defaults: +**Reasoning effort:** each dispatch passes `--request-effort ''`, which +outranks `GSTACK_CODEX_EFFORT`, then `codex_effort`, then the per-mode defaults: - Review (2A): `high` — bounded diff input, needs thoroughness - Challenge (2B): `high` — adversarial but bounded by diff - Consult (2C): `medium` — large context, interactive, needs speed @@ -788,18 +794,14 @@ Native `codex review` selects with `review` and sets both `model` and `review_mo flag also keeps installed skills out of Codex's context, so a review cannot become a nested skill run. -**Reasoning effort (per-mode defaults):** -- **Review (2A):** `high` — bounded diff input, needs thoroughness but not max tokens -- **Challenge (2B):** `high` — adversarial but bounded by diff size -- **Consult (2C):** `medium` — large context (plans, codebase), interactive, needs speed - -`run-with-timeout --effort ` resolves `GSTACK_CODEX_EFFORT`, then `gstack-config -set codex_effort `, then the default; an invalid value stops before any Codex call. +**Reasoning effort** (defaults in Step 1): `run-with-timeout --effort ` resolves +`--request-effort`, `GSTACK_CODEX_EFFORT`, `codex_effort`, then the default; an invalid +value stops before any Codex call. `--user-invoked` runs even with `codex_reviews` disabled: the user asked by name. -`xhigh` uses ~23x more tokens than `high` and causes 50+ minute hangs on large context -tasks (OpenAI issues #8545, #8402, #6931). Users can override with `--xhigh` flag -(e.g., `/codex review --xhigh`) when they want maximum reasoning and are willing to wait. +Override one run with `--effort ` (`/codex review --effort medium`). `xhigh` +uses ~23x more tokens than `high` and causes 50+ minute hangs on large context tasks +(OpenAI issues #8545, #8402, #6931). **Web search:** All codex commands pass `-c 'web_search="cached"'` so `codex exec` invocations can look up docs and APIs during review. This is OpenAI's cached index — diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index dd2b33854..9f55d3052 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -77,10 +77,17 @@ Only for an explicit `--role plan-review` (e.g. `/codex challenge --role plan-re set `_CODEX_ROLE='plan-review'` below and in the mode block; the mode block then probes and dispatches the policy model within its own timeout. [Policy setup](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md). +**Effort flags, read before the mode:** `--effort `, `--effort=` or `--xhigh` +(= `--effort xhigh`) directly after `/codex` or its mode word, among its leading flags +(`-m`, `--model`, `--role`) and before any prompt text, set `` (else `''`); +remove them before Step 1. A later `--effort` is prompt text; if a level follows, say so. Missing +value, not one lowercase word, or differing levels: stop, name the levels; nothing is sent. + ```bash _CODEX_PROBE=~/.claude/skills/gstack/bin/gstack-codex-probe _CODEX_ROLE='' [ -x "$_CODEX_PROBE" ] || { echo "HELPER_UNAVAILABLE"; exit 1; } +"$_CODEX_PROBE" check-effort high --request '' >/dev/null || exit 1 # free {{OUTSIDE_SELF_GUARD:codex}} if ! "$_CODEX_PROBE" check-auth >/dev/null; then @@ -92,6 +99,8 @@ fi "$_CODEX_PROBE" check-version # warns if known-bad, non-blocking ``` +If `check-effort` failed, relay its lines and stop; no Codex call was made. + 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: `gstack-codex-probe` is missing or @@ -155,7 +164,7 @@ After this, every subsequent bash block in this skill uses `"$PLAN_ROOT"` and ## Step 1: Detect mode -Parse the user's input to determine which mode to run: +Parse the input, effort flags removed, to determine which mode to run: 1. `/codex review` or `/codex review ` — **Review mode** (Step 2A) 2. `/codex challenge` or `/codex challenge ` — **Challenge mode** (Step 2B) @@ -181,11 +190,8 @@ The three modes are MUTUALLY EXCLUSIVE — at most one runs per invocation. Once the mode is determined, read ONLY that mode's section (see the Section index above); never read the other two mode sections. -**Reasoning effort override:** If the user's input contains `--xhigh` anywhere, -note it and remove it from the prompt text before passing to Codex. When `--xhigh` -is present, prefix the `run-with-timeout` dispatch with `GSTACK_CODEX_EFFORT=xhigh`, -which outranks `codex_effort` and the per-mode default. Otherwise `--effort` applies -`GSTACK_CODEX_EFFORT`, then `codex_effort`, then the per-mode defaults: +**Reasoning effort:** each dispatch passes `--request-effort ''`, which +outranks `GSTACK_CODEX_EFFORT`, then `codex_effort`, then the per-mode defaults: - Review (2A): `high` — bounded diff input, needs thoroughness - Challenge (2B): `high` — adversarial but bounded by diff - Consult (2C): `medium` — large context, interactive, needs speed @@ -248,18 +254,14 @@ Native `codex review` selects with `review` and sets both `model` and `review_mo flag also keeps installed skills out of Codex's context, so a review cannot become a nested skill run. -**Reasoning effort (per-mode defaults):** -- **Review (2A):** `high` — bounded diff input, needs thoroughness but not max tokens -- **Challenge (2B):** `high` — adversarial but bounded by diff size -- **Consult (2C):** `medium` — large context (plans, codebase), interactive, needs speed - -`run-with-timeout --effort ` resolves `GSTACK_CODEX_EFFORT`, then `gstack-config -set codex_effort `, then the default; an invalid value stops before any Codex call. +**Reasoning effort** (defaults in Step 1): `run-with-timeout --effort ` resolves +`--request-effort`, `GSTACK_CODEX_EFFORT`, `codex_effort`, then the default; an invalid +value stops before any Codex call. `--user-invoked` runs even with `codex_reviews` disabled: the user asked by name. -`xhigh` uses ~23x more tokens than `high` and causes 50+ minute hangs on large context -tasks (OpenAI issues #8545, #8402, #6931). Users can override with `--xhigh` flag -(e.g., `/codex review --xhigh`) when they want maximum reasoning and are willing to wait. +Override one run with `--effort ` (`/codex review --effort medium`). `xhigh` +uses ~23x more tokens than `high` and causes 50+ minute hangs on large context tasks +(OpenAI issues #8545, #8402, #6931). **Web search:** All codex commands pass `{{CODEX_WEB_SEARCH_FLAG}}` so `codex exec` invocations can look up docs and APIs during review. This is OpenAI's cached index — diff --git a/codex/sections/challenge-mode.md b/codex/sections/challenge-mode.md index ac43d023e..b7e39c5b6 100644 --- a/codex/sections/challenge-mode.md +++ b/codex/sections/challenge-mode.md @@ -55,7 +55,7 @@ if [ -n "$_CODEX_ROLE" ]; then export _CODEX_DEADLINE=$(($(date +%s)+540)); _COD else _CODEX_OUT=$("$_CODEX_PROBE" select-model exec) || exit 1; fi _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" run-with-timeout --user-invoked --effort high 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -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 --user-invoked --effort high --request-effort '' 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -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 diff --git a/codex/sections/challenge-mode.md.tmpl b/codex/sections/challenge-mode.md.tmpl index 16bbf84a9..4fa7ddb6a 100644 --- a/codex/sections/challenge-mode.md.tmpl +++ b/codex/sections/challenge-mode.md.tmpl @@ -41,7 +41,7 @@ fi [ -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; } {{CODEX_SELECT:exec:540}} -"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} {{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 --user-invoked --effort high --request-effort '' 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} {{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 diff --git a/codex/sections/consult-mode.md b/codex/sections/consult-mode.md index 12d3bcb1b..dda883f60 100644 --- a/codex/sections/consult-mode.md +++ b/codex/sections/consult-mode.md @@ -114,9 +114,9 @@ fi _LABEL=consult${_SID:+-resume} # Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper) if [ -n "$_SID" ]; then - "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium --request-effort '' 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" else - "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium --request-effort '' 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -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 diff --git a/codex/sections/consult-mode.md.tmpl b/codex/sections/consult-mode.md.tmpl index 8c6c3bb48..fa6587212 100644 --- a/codex/sections/consult-mode.md.tmpl +++ b/codex/sections/consult-mode.md.tmpl @@ -100,9 +100,9 @@ fi _LABEL=consult${_SID:+-resume} # Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper) if [ -n "$_SID" ]; then - "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" {{CODEX_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium --request-effort '' 540 codex exec resume "$_SID" - -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" {{CODEX_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" else - "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$PROMPT_FILE" 2>"$TMPERR" + "$_CODEX_PROBE" run-with-timeout --user-invoked --effort medium --request-effort '' 540 codex exec - -C "$_REPO_ROOT" -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} {{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 diff --git a/codex/sections/review-mode.md b/codex/sections/review-mode.md index ebf8f1a12..1c17d3d91 100644 --- a/codex/sections/review-mode.md +++ b/codex/sections/review-mode.md @@ -56,7 +56,7 @@ _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". -"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high 330 codex review --base -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "review_model=\"${_CODEX_SEL:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' < /dev/null >"$TMPOUT" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high --request-effort '' 330 codex review --base -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" -c "review_model=\"${_CODEX_SEL:?}\"" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' < /dev/null >"$TMPOUT" 2>"$TMPERR" _CODEX_EXIT=$? cat "$TMPOUT" if [ "$_CODEX_EXIT" = "124" ]; then @@ -123,7 +123,7 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp git diff "...HEAD" 2>/dev/null printf '\nDIFF_END\n' } > "$_PROMPT_FILE" -"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high 330 codex exec - -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high --request-effort '' 330 codex exec - -s "${_CODEX_SANDBOX_MODE:?}" -c "model=\"${_CODEX_SEL:?}\"" -c skills.include_instructions=false -c 'web_search="cached"' --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" _CODEX_EXIT=$? rm -f "$_PROMPT_FILE" "$FOCUS_FILE" cat "$TMPOUT" diff --git a/codex/sections/review-mode.md.tmpl b/codex/sections/review-mode.md.tmpl index 34eb451cb..bf0e41380 100644 --- a/codex/sections/review-mode.md.tmpl +++ b/codex/sections/review-mode.md.tmpl @@ -49,7 +49,7 @@ cd "$_REPO_ROOT" # 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". -"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high 330 codex review --base -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" {{CODEX_REVIEW_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} < /dev/null >"$TMPOUT" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high --request-effort '' 330 codex review --base -c "sandbox_mode=\"${_CODEX_SANDBOX_MODE:?}\"" {{CODEX_REVIEW_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} < /dev/null >"$TMPOUT" 2>"$TMPERR" _CODEX_EXIT=$? cat "$TMPOUT" if [ "$_CODEX_EXIT" = "124" ]; then @@ -104,7 +104,7 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp git diff "...HEAD" 2>/dev/null printf '\nDIFF_END\n' } > "$_PROMPT_FILE" -"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high 330 codex exec - -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" +"$_CODEX_PROBE" run-with-timeout --user-invoked --effort high --request-effort '' 330 codex exec - -s "${_CODEX_SANDBOX_MODE:?}" {{CODEX_MODEL_CONFIG_FLAG}} {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR" _CODEX_EXIT=$? rm -f "$_PROMPT_FILE" "$FOCUS_FILE" cat "$TMPOUT" diff --git a/docs/model-policy.md b/docs/model-policy.md index 7a9459485..b3ecd6e2c 100644 --- a/docs/model-policy.md +++ b/docs/model-policy.md @@ -123,11 +123,12 @@ explicit implementation model rather than delegating to a hidden native choice. ## Reasoning effort Codex calls carry a `model_reasoning_effort`. It follows the same shape as the -model: a per-run environment override, then a stored setting, then the default -each gate ships with. +model: a per-request flag on `/codex`, a per-run environment override, then a +stored setting, then the default each gate ships with. | Order | Source | Set it with | | --- | --- | --- | +| 0 | `/codex ... --effort ` (this request only; `/codex` alone) | `/codex review --effort medium` | | 1 | `GSTACK_CODEX_EFFORT` (this shell, this run) | `export GSTACK_CODEX_EFFORT=max` | | 2 | `codex_effort` in config.yaml | `gstack-config set codex_effort low` (`unset` restores the defaults) | | 3 | the gate's default | nothing: `high` for reviews, plan reviews and `/codex` review and challenge; `medium` for `/codex` consult and design direction | @@ -138,9 +139,15 @@ through the usual model error. An invalid value never reaches Codex: `gstack-config set` rejects it, and `gstack-codex-probe run-with-timeout --effort ` (which resolves the order above and appends the flag) stops the call before anything is sent, naming the source to fix. `gstack-codex-probe -check-effort ` prints the same resolution without a call. `/codex ... ---xhigh` outranks all three for that request (it sets `GSTACK_CODEX_EFFORT=xhigh` -on the dispatch). +check-effort ` prints the same resolution without a call. + +For one request, `/codex review --effort medium` (or `--effort=`; `--xhigh` is +shorthand for `--effort xhigh`) outranks the other three, including an exported +`GSTACK_CODEX_EFFORT`, and changes nothing that persists. The flag counts only +directly after `/codex` or its mode word, among the leading flags (`-m`/`--model`, +`--role`, in any order) and before the prompt text; a later `--effort` is part of the +prompt, and `/codex` says so. A Consult follow-up resolves its effort afresh. `/codex` passes the level to the probe as `--request-effort`, +so an unknown level stops before any Codex call, naming the flag. `/spec`'s quality gate keeps `medium` inside its 120-second budget whatever you set, because a higher effort would spend the time the gate is sized for. diff --git a/docs/skills.md b/docs/skills.md index 16a3e34bd..d4e3990e0 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -1193,7 +1193,9 @@ selection. See [model policy](model-policy.md) for pins, precedence and recovery A severity-gate PASS is separate from [review freshness](#review-readiness-dashboard): unresolved recorded findings (`findings > findings_fixed`, with missing `findings_fixed` treated as zero) prevent CURRENT even when the gate passes. This does not change the severity gate. Fixes still require a new completed, unchanged review pass before the fixed tree can grade CURRENT. -**Challenge** — adversarial mode. Codex actively tries to break your code. It looks for edge cases, race conditions, security holes, and assumptions that would fail under load. Uses maximum reasoning effort (`xhigh`). Think of it as a penetration test for your logic. +**Challenge** — adversarial mode. Codex actively tries to break your code. It looks for edge cases, race conditions, security holes, and assumptions that would fail under load. Think of it as a penetration test for your logic. + +**Reasoning effort** — Review and Challenge run at `high`, Consult at `medium`. `gstack-config set codex_effort ` changes that for every run; `--effort ` right after `/codex` or its mode word, among the leading flags (`-m`, `--model`, `--role`), changes it for one (`/codex review --effort medium` for a faster, cheaper review). `--xhigh` is shorthand for `--effort xhigh`. An unknown level stops before Codex runs. See [model policy](model-policy.md#reasoning-effort) for levels and precedence. **Consult** — open conversation with session continuity. Ask Codex anything about the codebase. Follow-up questions reuse the same session, so context carries over. Great for "am I thinking about this correctly?" moments. diff --git a/lib/model-policy-cli.ts b/lib/model-policy-cli.ts index 7626693d6..81700082a 100644 --- a/lib/model-policy-cli.ts +++ b/lib/model-policy-cli.ts @@ -66,9 +66,10 @@ Settings (gstack-config set|unset ): codex_effort ${CODEX_EFFORT_LEVELS.join(' | ')} (default: each gate's own effort) -Codex reasoning effort: GSTACK_CODEX_EFFORT (per run), then codex_effort, then -the gate's default (high for reviews, medium for /codex consult and design -direction). /spec keeps medium inside its 120 s budget. +Codex reasoning effort: /codex --effort (one request), then +GSTACK_CODEX_EFFORT (per run), then codex_effort, then the gate's default (high +for reviews, medium for /codex consult and design direction). /spec keeps medium +inside its 120 s budget. A detected custom Anthropic or OpenAI endpoint (Bedrock, Vertex, Foundry, a base-URL override or a Codex model_provider) never receives a shipped public ID: set a diff --git a/package.json b/package.json index d4d689e83..eb0c96206 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.93", + "version": "1.91.94", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/binding-template-drift.test.ts b/test/binding-template-drift.test.ts index 8dda8e0dc..732703775 100644 --- a/test/binding-template-drift.test.ts +++ b/test/binding-template-drift.test.ts @@ -120,7 +120,9 @@ 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('run-with-timeout --user-invoked --effort high 330 codex review')); + const nativeReview = codex.indexOf(`run-with-timeout --user-invoked --effort high --request-effort '' 330 codex review`); + expect(nativeReview).toBeGreaterThan(-1); + expect(starts[0].index).toBeLessThan(nativeReview); expect(starts[1].index).toBeLessThan(codex.indexOf('git diff "...HEAD"')); expect(codex).toContain('--finish CODEX_REVIEW_START'); expect(codex).toContain('"completed":COMPLETED,"converged":CONVERGED'); diff --git a/test/codex-effort.test.ts b/test/codex-effort.test.ts index 9ff3d1556..39d0f3bb1 100644 --- a/test/codex-effort.test.ts +++ b/test/codex-effort.test.ts @@ -14,6 +14,7 @@ import * as path from 'node:path'; import { codexEffortSetting, modelsMain } from '../lib/model-policy-cli'; import { outsideVoiceCommand } from '../scripts/resolvers/outside-voice'; import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types'; +import { between, expectMentions, expectTokens } from './helpers/prompt-structure'; const ROOT = path.resolve(import.meta.dir, '..'); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-effort-')); @@ -26,19 +27,42 @@ describe('manual /codex modes (#3098)', () => { test('every mode dispatches through run-with-timeout --effort with its own default', () => { for (const [mode, def] of MODES) { const tmpl = read(`codex/sections/${mode}.md.tmpl`); - const dispatches = tmpl.split('\n').filter(line => /run-with-timeout --user-invoked --effort \w+ \d+ codex (exec|review)\b/.test(line)); + const dispatches = tmpl.split('\n').filter(line => /run-with-timeout --user-invoked --effort \w+ (--request-effort '' )?\d+ codex (exec|review)\b/.test(line)); expect(dispatches.length, mode).toBeGreaterThan(0); for (const line of dispatches) { - expect(line, mode).toContain(`--effort ${def} `); + expect(line, mode).toContain(`--effort ${def} --request-effort '' `); expect(line, mode).not.toContain('model_reasoning_effort'); } expect(tmpl, mode).not.toContain('check-effort'); } }); - test('the skill body documents the precedence and the --xhigh override', () => { + test('the skill body documents the precedence and the --effort / --xhigh overrides', () => { const body = read('codex/SKILL.md.tmpl'); - for (const want of ['GSTACK_CODEX_EFFORT=xhigh', 'codex_effort', 'run-with-timeout --effort ']) expect(body).toContain(want); + for (const want of ['GSTACK_CODEX_EFFORT', 'codex_effort', 'run-with-timeout --effort ', '--request-effort']) expect(body).toContain(want); + const flags = between(body, '**Effort flags, read before the mode:**', '```bash'); + expectTokens(flags, ['--effort ', '--effort=', '--xhigh', '--effort xhigh', ''], 'codex effort flags'); + expectMentions(flags, [['directly after', 'mode word', 'leading flags', '--model', '--role', 'before any prompt text'], ['later', 'prompt text'], ['remove', 'before Step 1'], + ['missing', 'lowercase word', 'differing levels', 'stop'], ['later', 'level', 'say so']], 'codex effort flags'); + const step1 = between(body, '## Step 1: Detect mode', '- Review (2A)'); + expectMentions(step1, [['effort flags removed'], ['--request-effort', 'outranks']], 'codex Step 1'); + }); + + test('effort flags are read and validated before mode detection and before the paid model probe', () => { + const body = read('codex/SKILL.md.tmpl'); + const at = (s: string) => { const i = body.indexOf(s); expect(i, s).toBeGreaterThan(-1); return i; }; + const check = at(`"$_CODEX_PROBE" check-effort high --request '' >/dev/null || exit 1`); + expect(at('**Effort flags, read before the mode:**')).toBeLessThan(check); + expect(check).toBeLessThan(at('"$_CODEX_PROBE" probe-model exec')); + expect(check).toBeLessThan(at('## Step 1: Detect mode')); + expect(body).not.toMatch(/prefix each\s+`run-with-timeout` dispatch with `GSTACK_CODEX_EFFORT/); + }); + + test('docs/model-policy.md documents the per-request --effort flag in its Reasoning effort section', () => { + const doc = between(read('docs/model-policy.md'), '## Reasoning effort', '## Which workflows use the tiers?'); + expectTokens(doc, ['/codex review --effort medium', '--effort=', '--xhigh', '--request-effort', 'GSTACK_CODEX_EFFORT'], 'model-policy effort'); + expectMentions(doc, [['outranks', 'exported'], ['directly after', 'mode word', 'leading flags', 'prompt'], ['unknown level', 'before any Codex call']], 'model-policy effort'); + expect(doc).toContain('| 0 | `/codex ... --effort `'); }); }); @@ -97,6 +121,71 @@ echo '{"type":"item.completed","item":{"type":"agent_message","text":"SCORE: 8"} expect(dispatchedArgs('review', 'banana', undefined)).toEqual([]); }); + const FENCES = (['review-mode', 'challenge-mode', 'consult-mode'] as const).flatMap(mode => + read(`codex/sections/${mode}.md`).split('\n').filter(line => /run-with-timeout --user-invoked --effort \w+ --request-effort '' \d+ codex /.test(line)) + .map(line => ({ mode, line: line.trim().split(' | tee ')[0] }))); + + test('all five rendered /codex dispatches carry the request level to the probe, over env and codex_effort', () => { + expect(FENCES.length).toBe(5); + const state = path.join(TMP, 'state-codex-request-effort'); + fs.mkdirSync(state, { recursive: true }); + fs.writeFileSync(path.join(state, 'config.yaml'), 'codex_effort: low\n'); + const env: Record = {}; + for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(CODEX_|GSTACK_CODEX_|OPENAI_)/.test(k)) env[k] = v; + Object.assign(env, { PATH: `${fakeBin}:${process.env.PATH}`, GSTACK_HOME: state, GSTACK_STATE_ROOT: '', CODEX_HOME: TMP, GSTACK_ACTIVE_HOST: 'claude', CLAUDECODE: '1' }); + const err = path.join(TMP, 'dispatch-err'); + const run = (line: string, level: string, exported?: string) => { + fs.rmSync(capture, { force: true }); + fs.rmSync(err, { force: true }); + const script = [`_CODEX_PROBE=${JSON.stringify(path.join(ROOT, 'bin', 'gstack-codex-probe'))}`, '_CODEX_SANDBOX_MODE=read-only', '_CODEX_SEL=gpt-test', + `_REPO_ROOT=${JSON.stringify(repo)}`, `PROMPT_FILE=${JSON.stringify(prompt)}`, `_PROMPT_FILE=${JSON.stringify(prompt)}`, '_SID=sid-test', + `TMPOUT=${JSON.stringify(path.join(TMP, 'dispatch-out'))}`, `TMPRESP=${JSON.stringify(path.join(TMP, 'dispatch-resp'))}`, `TMPERR=${JSON.stringify(err)}`, + line.replace('', 'HEAD').replace(`''`, `'${level}'`)].join('\n'); + const r = spawnSync('bash', ['-c', script], { cwd: repo, env: exported ? { ...env, GSTACK_CODEX_EFFORT: exported } : env, encoding: 'utf8', timeout: 60_000 }); + return { status: r.status, args: fs.existsSync(capture) ? fs.readFileSync(capture, 'utf8').split('\n') : [], stderr: fs.existsSync(err) ? fs.readFileSync(err, 'utf8') : '' }; + }; + for (const { mode, line } of FENCES) { + expect(run(line, '').args, mode).toContain('model_reasoning_effort="low"'); + expect(run(line, 'medium').args, mode).toContain('model_reasoning_effort="medium"'); + expect(run(line, 'xhigh').args, mode).toContain('model_reasoning_effort="xhigh"'); + expect(run(line, 'medium', 'max').args, mode).toContain('model_reasoning_effort="medium"'); + const unfilled = run(line, ''); + expect(unfilled.args, mode).toEqual([]); + expect(unfilled.stderr, mode).toContain('placeholder was left unfilled'); + const bogus = run(line, 'bogus'); + expect(bogus.args, mode).toEqual([]); + expect(bogus.status, mode).toBe(1); + expect(bogus.stderr, mode).toContain("CODEX_EFFORT: invalid — 'bogus' from /codex --effort"); + expect(bogus.stderr, mode).toContain('--effort medium'); + } + }); + + test('check-effort --request outranks env and config, and names the flag when the level is invalid', () => { + const probe = path.join(ROOT, 'bin', 'gstack-codex-probe'); + const state = path.join(TMP, 'state-check-effort-request'); + fs.mkdirSync(state, { recursive: true }); + fs.writeFileSync(path.join(state, 'config.yaml'), 'codex_effort: low\n'); + const env: Record = {}; + for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(CODEX_|GSTACK_CODEX_|OPENAI_)/.test(k)) env[k] = v; + Object.assign(env, { GSTACK_HOME: state, GSTACK_STATE_ROOT: '' }); + const check = (args: string[], exported?: string) => spawnSync(probe, ['check-effort', ...args], { env: exported ? { ...env, GSTACK_CODEX_EFFORT: exported } : env, encoding: 'utf8', timeout: 30_000 }); + expect(check(['high', '--request', 'medium'], 'max').stdout).toBe('CODEX_EFFORT: medium\n'); + expect(check(['high', '--request', '']).stdout).toBe('CODEX_EFFORT: low\n'); + expect(check(['high', '--request', ''], 'max').stdout).toBe('CODEX_EFFORT: max\n'); + const bad = check(['high', '--request', 'turbo']); + expect(bad.status).toBe(1); + expect(bad.stdout).toBe(''); + expect(bad.stderr).toContain("'turbo' from /codex --effort"); + expect(bad.stderr).not.toContain('export GSTACK_CODEX_EFFORT'); + expect(bad.stderr).toContain('docs/model-policy.md#reasoning-effort'); + const unfilled = check(['high', '--request', '']); + expect(unfilled.status).toBe(1); + expect(unfilled.stderr).toContain("'' placeholder was left unfilled"); + expect(unfilled.stderr).not.toContain('from /codex --effort'); + expect(check(['high', '--request']).status).toBe(64); + expect(check(['high', '--bogus', 'low']).status).toBe(64); + }); + test('/spec keeps medium effort and its 120 s budget whatever env or config say', () => { for (const [env, stored] of [[undefined, undefined], ['max', undefined], [undefined, 'xhigh'], ['ultra', 'low']] as const) { expect(dispatchedArgs('spec', env, stored)).toContain('model_reasoning_effort="medium"'); diff --git a/test/codex-hardening.test.ts b/test/codex-hardening.test.ts index 0810f6d6d..00434948e 100644 --- a/test/codex-hardening.test.ts +++ b/test/codex-hardening.test.ts @@ -645,7 +645,7 @@ describe('codex timeout wrapper: /review + /ship diff passes', () => { test(`${relPath}: both diff-review Codex calls run under the wrapper`, () => { const wrapped = - read().match(/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+)?\d+\s+codex\s+(exec|review)\b/g) ?? []; + read().match(/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+(?:--request-effort\s+'[^']*'\s+)?)?\d+\s+codex\s+(exec|review)\b/g) ?? []; // Adversarial pass + structured review pass. expect(wrapped.length).toBeGreaterThanOrEqual(2); }); @@ -658,7 +658,7 @@ describe('codex timeout wrapper: /review + /ship diff passes', () => { }); test(`${relPath}: wrapper budget stays under the outer Bash gate`, () => { - const budgets = [...read().matchAll(/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+)?(\d+)\s+codex\b/g)].map( + const budgets = [...read().matchAll(/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+(?:--request-effort\s+'[^']*'\s+)?)?(\d+)\s+codex\b/g)].map( (m) => Number(m[1]) * 1000, ); expect(budgets.length).toBeGreaterThan(0); @@ -710,7 +710,7 @@ 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) => /run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+)?\d+\s+codex\s+review\b/.test(l)); + .filter((l) => /run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+(?:--request-effort\s+'[^']*'\s+)?)?\d+\s+codex\s+review\b/.test(l)); expect(invocations.length).toBeGreaterThanOrEqual(1); for (const line of invocations) { // select-model reports read-only (full access only for @@ -772,7 +772,7 @@ describe('codex skeleton+sections union: review sandbox + fail-closed gate + tim 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(/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+)?(\d+)\s+codex\b/g)].map( + const wrappers = [...section.matchAll(/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+(?:--request-effort\s+'[^']*'\s+)?)?(\d+)\s+codex\b/g)].map( (m) => Number(m[1]) * 1000, ); if (gates.length === 0 || wrappers.length === 0) continue; diff --git a/test/codex-invocation-sites.test.ts b/test/codex-invocation-sites.test.ts index 290834ec5..05a2e4b57 100644 --- a/test/codex-invocation-sites.test.ts +++ b/test/codex-invocation-sites.test.ts @@ -17,7 +17,7 @@ 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 probe's run-with-timeout. */ -const COMMAND = /(?:^|[;&|]\s*|\$\(\s*)(?:"\$_CODEX_PROBE"\s+run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+)?\d+\s+)?codex\s+(?:exec|review)\b/; +const COMMAND = /(?:^|[;&|]\s*|\$\(\s*)(?:"\$_CODEX_PROBE"\s+run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+(?:--request-effort\s+'[^']*'\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 } diff --git a/test/codex-skill-isolation.test.ts b/test/codex-skill-isolation.test.ts index a014a085a..fb9490971 100644 --- a/test/codex-skill-isolation.test.ts +++ b/test/codex-skill-isolation.test.ts @@ -43,7 +43,7 @@ describe('Codex skill isolation (#2847)', () => { const calls: string[] = []; for (const file of renderedSkillFiles()) { for (const line of fs.readFileSync(file, 'utf8').split('\n')) { - if (/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+)?\d+\s+codex\s+(exec|review)\b/.test(line)) calls.push(`${path.relative(ROOT, file)}: ${line}`); + if (/run-with-timeout\s+(?:--user-invoked\s+)?(?:--effort\s+\w+\s+(?:--request-effort\s+'[^']*'\s+)?)?\d+\s+codex\s+(exec|review)\b/.test(line)) calls.push(`${path.relative(ROOT, file)}: ${line}`); } } expect(calls.length).toBeGreaterThan(10); diff --git a/test/helpers/placeholder-allowlist.ts b/test/helpers/placeholder-allowlist.ts index 2554ac6d9..6d1b86564 100644 --- a/test/helpers/placeholder-allowlist.ts +++ b/test/helpers/placeholder-allowlist.ts @@ -41,6 +41,7 @@ export const IDENTIFIER_PLACEHOLDERS: Record = { '': { grammar: oneOf('clean', 'flagged'), what: 'semantic review outcome' }, '': { grammar: /^(?:[0-9]+(?:\.[0-9]+)?|null)$/, what: 'health score or null' }, '': { grammar: oneOf('github', 'gitlab', 'unknown'), what: 'detected git platform' }, + '': { grammar: /^[a-z]*$/, what: "the request's /codex --effort level, or empty for none (the probe validates the level)", quoted: true }, // Ids, keys and numbers. '': { grammar: ID, what: 'question or record id (kebab-case)' }, diff --git a/test/model-role-invocation.test.ts b/test/model-role-invocation.test.ts index d9be98072..1388574de 100644 --- a/test/model-role-invocation.test.ts +++ b/test/model-role-invocation.test.ts @@ -563,6 +563,7 @@ describe('manual /codex entry: explicit role only, paid probe only for the dispa const preflight = skill.match(/```bash\n(_CODEX_PROBE=[^\n]*\n_CODEX_ROLE=''[\s\S]*?)\n```/)![1]! .replace('{{OUTSIDE_SELF_GUARD:codex}}', RESOLVERS.OUTSIDE_SELF_GUARD(ctxFor('codex', 'claude', HOST_PATHS.claude), ['codex'])); const withRole = (text: string, role: string) => text.replaceAll("_CODEX_ROLE=''", `_CODEX_ROLE='${role}'`); + const withEffort = (text: string, level: string) => text.replaceAll(`''`, `'${level}'`); const shell = (h: { dir: string; state: string }, script: string, sh = 'bash') => { fs.mkdirSync(path.join(h.dir, '.claude', 'skills'), { recursive: true }); if (!fs.existsSync(path.join(h.dir, '.claude', 'skills', 'gstack'))) fs.symlinkSync(ROOT, path.join(h.dir, '.claude', 'skills', 'gstack')); @@ -574,17 +575,33 @@ describe('manual /codex entry: explicit role only, paid probe only for the dispa test('Step 0.5 probes the native model without a role and makes no model call with it', () => { expect(skill).toContain('[Policy setup](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md)'); - const plain = shell(home(), preflight); + const plain = shell(home(), withEffort(preflight, '')); expect(plain.status).toBe(0); expect(plain.stdout).toContain('MODEL_OK'); expect(plain.calls.map(codexModel)).toEqual(['model="gpt-5.6-terra"']); - const role = shell(home(), withRole(preflight, 'plan-review')); + const role = shell(home(), withEffort(withRole(preflight, 'plan-review'), '')); expect(role.status).toBe(0); expect(role.stdout).not.toMatch(/MODEL_OK|MODEL_UNUSABLE|AUTH_FAILED/); expect(role.calls).toEqual([]); expect(role.stderr).not.toContain('NOTICE: gstack plan reviews'); }); + test('a valid /codex --effort level passes Step 0.5, shows its source once and probes once', () => { + const r = shell(home(), withEffort(preflight, 'medium')); + expect(r.status).toBe(0); + expect(r.stdout).not.toContain('CODEX_EFFORT'); + expect(r.stderr).toContain('CODEX_EFFORT: medium (source: /codex --effort)'); + expect(r.calls.map(codexModel)).toEqual(['model="gpt-5.6-terra"']); + }); + + test('an invalid /codex --effort level stops Step 0.5 before the paid model probe', () => { + const r = shell(home(), withEffort(preflight, 'turbo')); + expect(r.status).toBe(1); + expect(r.stderr).toContain("'turbo' from /codex --effort"); + expect(r.stdout).not.toMatch(/MODEL_OK|MODEL_UNUSABLE|AUTH_FAILED/); + expect(r.calls).toEqual([]); + }); + test('a missing manual challenge prompt stops before the paid readiness probe or notice', () => { const h = home(); const template = fs.readFileSync(path.join(ROOT, 'codex/sections/challenge-mode.md.tmpl'), 'utf8'); @@ -607,7 +624,7 @@ describe('manual /codex entry: explicit role only, paid probe only for the dispa expect(template.match(/\{\{CODEX_SELECT/g)!.length).toBe(selections.length); for (const block of template.split(/\{\{CODEX_SELECT:/).slice(1)) { const [kind, budget] = block.split('}}')[0]!.split(':'); - const wrappers = [...block.split('```')[0]!.matchAll(/run-with-timeout (?:--user-invoked )?(?:--effort \w+ )?(\d+) codex/g)].map(m => m[1]); + const wrappers = [...block.split('```')[0]!.matchAll(/run-with-timeout (?:--user-invoked )?(?:--effort \w+ (?:--request-effort '[^']*' )?)?(\d+) codex/g)].map(m => m[1]); expect(wrappers.length).toBeGreaterThan(0); expect(new Set(wrappers)).toEqual(new Set([budget])); expect(kind).toMatch(/^(exec|review)$/); diff --git a/test/outside-send-boundary.test.ts b/test/outside-send-boundary.test.ts index 2c566a6a6..3e33250a2 100644 --- a/test/outside-send-boundary.test.ts +++ b/test/outside-send-boundary.test.ts @@ -17,7 +17,7 @@ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-send-boundary-')); afterAll(() => fs.rmSync(TMP, { recursive: true, force: true })); const POSITION = String.raw`(?:^|[;&|]\s*|\$\(\s*|\b(?:then|do|else|if|!)\s+)`; -const CODEX = new RegExp(`${POSITION}(?:g?timeout\\s+\\S+\\s+)?((?:"\\$_CODEX_PROBE"\\s+run-with-timeout\\s+(?:--user-invoked\\s+)?(?:--effort\\s+\\w+\\s+)?\\d+\\s+)?)codex\\s+(?:exec|review)\\b`); +const CODEX = new RegExp(`${POSITION}(?:g?timeout\\s+\\S+\\s+)?((?:"\\$_CODEX_PROBE"\\s+run-with-timeout\\s+(?:--user-invoked\\s+)?(?:--effort\\s+\\w+\\s+(?:--request-effort\\s+'[^']*'\\s+)?)?\\d+\\s+)?)codex\\s+(?:exec|review)\\b`); /** A headless Claude prompt run (`claude -p`, `--print`, or a prompt on stdin), or the resolved CLAUDE_BIN; `claude mcp|auth|config` manage the CLI and send no repository content. */ const CLAUDE = new RegExp(`${POSITION}(?:command\\s+)?(?:"\\$\\{?CLAUDE_BIN\\}?"|claude(?=\\s)(?![^|;&]*\\s(?:mcp|auth|config|update|doctor)\\b)[^|;&]*(?:\\s(?:-p|--print)\\b|\\s<\\s*\\S))`); @@ -59,7 +59,7 @@ describe('generated skills start outside CLIs only through the consent-enforcing expect(files.length).toBeGreaterThan(20); const bad = files.flatMap(f => directLaunches(fs.readFileSync(f, 'utf8')).map(l => `${path.relative(TMP, f)}: ${l}`)); expect(bad).toEqual([]); - const wrapped = files.filter(f => /run-with-timeout (?:--user-invoked )?(?:--effort \w+ )?\d+ codex|gstack-claude-code/.test(fs.readFileSync(f, 'utf8'))); + const wrapped = files.filter(f => /run-with-timeout (?:--user-invoked )?(?:--effort \w+ (?:--request-effort '[^']*' )?)?\d+ codex|gstack-claude-code/.test(fs.readFileSync(f, 'utf8'))); expect(wrapped.length).toBeGreaterThan(5); }); }