mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
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:
1 parent
7fab2a7139
commit
64e8f90f15
47 files changed
+937
-399
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user