mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
v1.91.16.0 fix: safe installs and upgrades, skills start everywhere, truthful completion (parallel wave) (#3014)
* fix(make-pdf,design): real TOC page numbers and anchors; design models move to gpt-5.5 + gpt-image-2 (#2903, #2807) * ci: pin every action to a SHA, Scorecard, checksummed Bun, scoped Codex CLI in the CI image, macOS named-regressions job (#1948, #1706, #1997) * fix(cso,browse): Windows/macOS lease identity, retention skips bad runs, truthful 'not assessed' reports, env.kv false positive, Windows browse spawn fixes (#2894, #2912, #2048, #2637, #2805) * fix(outside-voice): one Codex model precedence for probe and dispatch, bounded timeouts with KILL escalation, probe cache identity, guarded mktemp, skills hidden in nested Codex (#2776, #2914, #2787, #2881, #2847) * feat(setup): install registry, ./setup --status, staged activation, per-scope migrations, host tiers and conformance kit, OpenCode/Hermes fixes, Copilot CLI (experimental), disabled_skills, slimmer CLAUDE.md (#1925, #2879, #2347, #2338, #2626, #2629, #2825, #2015, #393, #1206, #2096) Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com> Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com> Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com> * fix(preamble): skills start in worktree-isolated Claude Code sessions — one literal skill-start line, context-recovery helper, no eval in skill blocks, $N token tripwire, honest plan-mode precedence (#2763, #2896, #2851) * fix(setup): per-install renders for renamed, vendored and custom-config installs, space-safe alias roots, per-host /cso launcher path (#1882, #2906) * test: free tests never write the real HOME (home-write tripwire), no orphaned browse daemons, symlinked-TMPDIR (macOS) fixes, gbrain-on-PATH isolation (#2895, #2816, #2829, #2841, #2982, #2911, #2909, #2908, #2910) * chore: integrate wave follow-ups — regenerate all hosts, README model docs, TODOS, touchfiles, duration seeds, Bun 1.3 telemetry test fix, replay fixtures pinned to their recorded bytes * v1.91.14.0: release metadata * test(browse): group-kill cleanup falls back to the leader when macOS returns EPERM for a live group * ci: Codex login steps run under bash (the CI container's default sh rejects pipefail) * fix: full-suite repairs — private state roots for 7 real-HOME writers, truthful home-guard attribution with --attribute-home, gstack-paths shim in ship-hook fixtures, Copilot in team-init probes, test-of-test detector, cookie review fixture pinned to the approved excerpt * fix(windows): LF for Dockerfiles, normalized home-guard exclusions, path.relative fixture containment, ship push test spawn timeouts * test(design-detect): widen the whole-scan budget test's timing margins so a loaded macOS runner can't time out the single-batch call * fix(evals): qa protocol names defined for the judge, async PTY judge so overlapping sessions keep their cleanup deadline, bws setup cases use the standard JUDGE_MS tier * test(pty): the non-blocking judge case is POSIX-only (its fake judge is a shebang script, like the PTY harness it covers) * test(evals): ship-coverage-audit names absolute skill paths, uses Read, and skips the Step 7 artifact nothing consumes (removes the mechanism behind 3 of 4 historical timeouts) * test: deflake terminal-agent concurrent startup (write the placeholder record before spawning, like the product) and qa-evidence resumed pipe (barrier on a full high-water mark instead of a sleep) * fix(design): daemon log stream handles open errors instead of crashing the daemon; shutdown test awaits the stubbed exit * test(evals): skillify checks the product's resolved state root (no GSTACK_HOME bypass), negative cases check every root; tpa shims live outside the agent's cwd and answer consistently * test(evals): coverage-audit detector splits parallel columns at │ rails and [+] groups; coverage prompts name absolute skill paths; TODOs from the gate eval census * fix(plan-eng-review): scope gate always sends the menu (no headless 'Scope pending' exit); PTY harness ends idle turn-ends and API-error panels promptly with a precise diagnostic * test(evals): one position-bound coverage-read rule credits read-only && chains (labels, context ranges, git displays in either order) and still rejects forged output * fix(ship): documentation section has one saved source inserted by cat (never retyped); spawned-marker evidence is the child output; refreshed codex/factory ship goldens * test(docsync): the byte-exact insert positive is POSIX-only (the bash-command observer refuses backslash paths by design) * test(evals): plan-design-with-ui focus-title rule accepts the comma-less 'dimensions or focus' wording (replayed on the captured CI run, negative controls kept) * fix(qa-callers): exploratory preflight says the scope/method Reads come after exploratory.md; design-review scan check credits an executed scan proven by one engine run over the changed files --------- Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com> Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com> Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>
This commit is contained in:
392 files changed
+10217
-3784
No files matched your search
+103
-31
@@ -4,7 +4,8 @@
|
||||
#
|
||||
# Functions (all prefixed with _gstack_codex_ for namespace hygiene):
|
||||
# _gstack_codex_auth_probe — multi-signal auth check (env + file)
|
||||
# _gstack_codex_model_probe — round-trip probe of gstack's selected model (#2477)
|
||||
# _gstack_codex_select_model — resolve + print the runtime model (#2914)
|
||||
# _gstack_codex_model_probe — round-trip probe of the selected model (#2477)
|
||||
# _gstack_codex_version_check — warn on known-bad Codex CLI versions
|
||||
# _gstack_codex_timeout_wrapper — gtimeout -> timeout -> bash-native watchdog
|
||||
# _gstack_codex_log_event — telemetry emission to ~/.gstack/analytics/
|
||||
@@ -34,24 +35,56 @@ _gstack_codex_auth_probe() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Runtime model selection (#2914) ---------------------------------------
|
||||
|
||||
_gstack_codex_select_model() {
|
||||
# One selection record per invocation kind, shared by the probe and the
|
||||
# dispatch flag in the same block: explicit request ($2), GSTACK_CODEX_MODEL,
|
||||
# Codex config.toml (review_model first for native review), gstack default.
|
||||
# The TOML is read by the TypeScript resolver; bash never parses it.
|
||||
local _kind="${1:-exec}" _explicit="${2:-}" _line _rc
|
||||
case "$_kind" in exec|review) ;; *) echo "ERROR: _gstack_codex_select_model takes exec or review" >&2; return 2 ;; esac
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
echo "CODEX_MODEL: unresolved — bun is not on PATH, so gstack cannot read the Codex model choice. Reinstall gstack with ./setup; no Codex call was made." >&2
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$_explicit" ]; then
|
||||
_line=$(bun run "${BASH_SOURCE[0]%/*}/../scripts/resolve-codex-generation-model.ts" --runtime "$_kind" --explicit "$_explicit")
|
||||
else
|
||||
_line=$(bun run "${BASH_SOURCE[0]%/*}/../scripts/resolve-codex-generation-model.ts" --runtime "$_kind")
|
||||
fi
|
||||
_rc=$?
|
||||
_GSTACK_CODEX_SEL="${_line%%$'\t'*}"
|
||||
_GSTACK_CODEX_SEL_SRC="${_line#*$'\t'}"
|
||||
case "$_GSTACK_CODEX_SEL" in ''|*[!A-Za-z0-9._:/-]*) _rc=1 ;; esac
|
||||
if [ "$_rc" -ne 0 ] || [ "${#_GSTACK_CODEX_SEL}" -gt 100 ]; then
|
||||
_GSTACK_CODEX_SEL=""
|
||||
_GSTACK_CODEX_SEL_SRC=""
|
||||
echo "CODEX_MODEL: invalid — Codex outside review unavailable; missing coverage. No Codex call was made. Fix the model choice above; gstack never substitutes its default for it." >&2
|
||||
return 1
|
||||
fi
|
||||
_GSTACK_CODEX_SEL_KIND="$_kind"
|
||||
echo "CODEX_MODEL: $_GSTACK_CODEX_SEL ($_kind; source: $_GSTACK_CODEX_SEL_SRC)" >&2
|
||||
}
|
||||
|
||||
# --- Model round-trip probe (#2477) ------------------------------------------
|
||||
|
||||
_gstack_codex_model_probe() {
|
||||
# Auth-exists is a weaker signal than the auth probe implies: a ChatGPT
|
||||
# account can be valid while the model gstack will request is unavailable.
|
||||
# A short real round trip with gstack's selected model catches model
|
||||
# rejection and entitlement changes in one shot (#2477). gstack pins a
|
||||
# frontier default rather than inheriting Codex CLI's built-in default,
|
||||
# because that default can lag the most capable available model.
|
||||
# A short real round trip with the selected model catches model rejection
|
||||
# and entitlement changes in one shot (#2477). The model comes from
|
||||
# _gstack_codex_select_model ($1 = exec|review, default exec), the same
|
||||
# record the dispatch flag uses, so the probe never checks a different model.
|
||||
#
|
||||
# Contract:
|
||||
# MODEL_OK (exit 0) — round trip succeeded; cached 1h.
|
||||
# MODEL_UNUSABLE (exit 1) — deterministic model 400; hints printed.
|
||||
# Cached 15 min: the 400 is model/entitlement-driven, so re-probing every preflight
|
||||
# charged the affected user a 30s round trip + real tokens per review
|
||||
# section, forever. Editing config.toml (the fix) changes the cache
|
||||
# signature and re-probes immediately; the short TTL covers server-side
|
||||
# entitlement recovery the signature can't see.
|
||||
# MODEL_UNUSABLE (exit 1) — deterministic model 400 or an invalid
|
||||
# model choice; hints printed. The 400 is cached 15 min: re-probing every
|
||||
# preflight charged the affected user a 30s round trip + real tokens per
|
||||
# review section, forever. Changing the model or config.toml changes the
|
||||
# cache signature and re-probes immediately; the short TTL covers
|
||||
# server-side entitlement recovery the signature can't see.
|
||||
# MODEL_UNUSABLE_INSTALL (exit 2) — the CLI cannot execute at all (spawn
|
||||
# ENOENT, non-executable binary, missing vendor payload). Deterministic,
|
||||
# so fail-open is wrong: retrying never helps. Never cached — a reinstall
|
||||
@@ -62,28 +95,45 @@ _gstack_codex_model_probe() {
|
||||
#
|
||||
# Only call this AFTER _gstack_codex_auth_probe passes — probing without
|
||||
# auth just measures the auth failure again.
|
||||
local _kind="${1:-exec}"
|
||||
if [ -z "${_GSTACK_CODEX_SEL:-}" ] || [ "${_GSTACK_CODEX_SEL_KIND:-}" != "$_kind" ]; then
|
||||
if ! _gstack_codex_select_model "$_kind"; then
|
||||
echo "MODEL_UNUSABLE"
|
||||
echo "HINT: the requested Codex model is not a valid model id; fix it as described above."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
local _model="$_GSTACK_CODEX_SEL"
|
||||
local _codex_home="${CODEX_HOME:-$HOME/.codex}"
|
||||
# State root from the shared twin (sourced in a subshell, so the caller's
|
||||
# shell is untouched). A missing twin only disables the cache.
|
||||
local _gstack_home _cache=""
|
||||
_gstack_home="$(. "${BASH_SOURCE[0]%/*}/gstack-state-root.sh" 2>/dev/null && gstack_state_root)" || _gstack_home=""
|
||||
[ -n "$_gstack_home" ] && _cache="$_gstack_home/.codex-model-probe"
|
||||
local _model="${GSTACK_CODEX_MODEL:-gpt-6-astra}"
|
||||
# Cache signature: config.toml + auth.json mtimes + gstack model selection.
|
||||
# Editing the model env/config or re-logging-in invalidates the cached result
|
||||
# immediately.
|
||||
# Cache signature (#2787): everything that can change the round trip's
|
||||
# outcome without the hour passing — the model, the Codex home and its
|
||||
# config.toml mtime, the auth mode (which env var or auth.json, plus its
|
||||
# mtime; never a credential value), and the resolved codex binary and its
|
||||
# mtime (an upgrade or PATH swap re-probes). Folded through cksum so paths
|
||||
# with spaces stay one field of the cache line.
|
||||
# GNU-first stat order + numeric validation (the #2195 pattern): on GNU
|
||||
# stat, `-f` means FILESYSTEM mode, so the BSD-first form emitted a
|
||||
# multi-line filesystem block on Linux — the signature then never matched
|
||||
# its own cache line and the cache missed on every read. BSD stat rejects
|
||||
# `-c` cleanly, so GNU-first degrades correctly on macOS.
|
||||
local _cfg_m _auth_m _model_sig _sig
|
||||
local _cfg_m _auth_m _auth_mode="" _bin _bin_m _sig
|
||||
_cfg_m=$(stat -c %Y "$_codex_home/config.toml" 2>/dev/null || stat -f %m "$_codex_home/config.toml" 2>/dev/null || echo 0)
|
||||
_auth_m=$(stat -c %Y "$_codex_home/auth.json" 2>/dev/null || stat -f %m "$_codex_home/auth.json" 2>/dev/null || echo 0)
|
||||
case "$_cfg_m" in ''|*[!0-9]*) _cfg_m=0 ;; esac
|
||||
case "$_auth_m" in ''|*[!0-9]*) _auth_m=0 ;; esac
|
||||
_model_sig=$(printf '%s' "$_model" | sed 's/[^A-Za-z0-9._:-]/_/g')
|
||||
_sig="${_cfg_m}-${_auth_m}-${_model_sig}"
|
||||
[ -n "$(printf '%s' "${CODEX_API_KEY:-}" | tr -d '[:space:]')" ] && _auth_mode="${_auth_mode}env-codex,"
|
||||
[ -n "$(printf '%s' "${OPENAI_API_KEY:-}" | tr -d '[:space:]')" ] && _auth_mode="${_auth_mode}env-openai,"
|
||||
[ -f "$_codex_home/auth.json" ] && _auth_mode="${_auth_mode}file,"
|
||||
_bin=$(command -v codex 2>/dev/null || echo none)
|
||||
_bin=$(readlink -f "$_bin" 2>/dev/null || printf '%s' "$_bin")
|
||||
_bin_m=$(stat -c %Y "$_bin" 2>/dev/null || stat -f %m "$_bin" 2>/dev/null || echo 0)
|
||||
case "$_bin_m" in ''|*[!0-9]*) _bin_m=0 ;; esac
|
||||
_sig=$(printf '%s\n' "$_model" "$_codex_home" "$_cfg_m" "$_auth_mode" "$_auth_m" "$_bin" "$_bin_m" | cksum | tr -d ' ')
|
||||
local _now
|
||||
_now=$(date +%s 2>/dev/null || echo 0)
|
||||
if [ -n "$_cache" ] && [ -f "$_cache" ]; then
|
||||
@@ -99,13 +149,13 @@ _gstack_codex_model_probe() {
|
||||
fi
|
||||
if [ "$_c_status" = "MODEL_UNUSABLE" ] && [ "$_c_sig" = "$_sig" ] && [ $((_now - _c_ts)) -lt 900 ]; then
|
||||
echo "MODEL_UNUSABLE (cached)"
|
||||
echo "HINT: gstack requested model '$_model'."
|
||||
echo "HINT: set GSTACK_CODEX_MODEL=<supported-model> or pass an explicit -c model=... override."
|
||||
echo "HINT: gstack requested model '$_model' (source: $_GSTACK_CODEX_SEL_SRC)."
|
||||
echo "HINT: choose a model your account can use: set GSTACK_CODEX_MODEL=<supported-model>, set model in $_codex_home/config.toml, or name one for this request."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
local _out _code
|
||||
_out=$(_gstack_codex_timeout_wrapper 30 codex exec --skip-git-repo-check -s read-only -c "model=\"$_model\"" "reply OK" </dev/null 2>&1)
|
||||
_out=$(_gstack_codex_timeout_wrapper 30 codex exec --skip-git-repo-check -s read-only -c "model=\"$_model\"" -c 'skills.include_instructions=false' "reply OK" </dev/null 2>&1)
|
||||
_code=$?
|
||||
if [ "$_code" -eq 0 ]; then
|
||||
[ -n "$_cache" ] && { mkdir -p "$_gstack_home" 2>/dev/null; printf 'MODEL_OK %s %s\n' "$_now" "$_sig" > "$_cache" 2>/dev/null; }
|
||||
@@ -116,8 +166,8 @@ _gstack_codex_model_probe() {
|
||||
[ -n "$_cache" ] && { mkdir -p "$_gstack_home" 2>/dev/null; printf 'MODEL_UNUSABLE %s %s\n' "$_now" "$_sig" > "$_cache" 2>/dev/null; }
|
||||
echo "MODEL_UNUSABLE"
|
||||
printf '%s\n' "$_out" | grep -i "model" | head -3
|
||||
echo "HINT: gstack requested model '$_model'."
|
||||
echo "HINT: set GSTACK_CODEX_MODEL=<supported-model> or pass an explicit -c model=... override."
|
||||
echo "HINT: gstack requested model '$_model' (source: $_GSTACK_CODEX_SEL_SRC)."
|
||||
echo "HINT: choose a model your account can use: set GSTACK_CODEX_MODEL=<supported-model>, set model in $_codex_home/config.toml, or name one for this request."
|
||||
_gstack_codex_log_event "codex_model_unusable" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
@@ -181,24 +231,46 @@ _gstack_codex_timeout_wrapper() {
|
||||
# Resolve wrapper binary: prefer gtimeout (Homebrew coreutils on macOS),
|
||||
# fall back to timeout (Linux), else a bash-native watchdog. Arguments:
|
||||
# $1 is the duration in seconds; rest is the command to run.
|
||||
# A child that ignores TERM is KILLed after a grace period
|
||||
# (_GSTACK_CODEX_KILL_AFTER seconds, default 10), so the deadline always
|
||||
# ends the provider before the caller's outer tool gate (#2776). Either
|
||||
# way a deadline reports timeout(1)'s exit 124; output already written stays.
|
||||
local _duration="$1"
|
||||
shift
|
||||
local _to
|
||||
local _grace="${_GSTACK_CODEX_KILL_AFTER:-10}"
|
||||
local _to _rc _start
|
||||
_to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "")
|
||||
if [ -n "$_to" ]; then
|
||||
"$_to" "$_duration" "$@"
|
||||
_start=$(date +%s 2>/dev/null || echo 0)
|
||||
"$_to" -k "$_grace" "$_duration" "$@"
|
||||
_rc=$?
|
||||
# 137 = timeout(1)'s KILL escalation; it is still the deadline firing.
|
||||
if [ "$_rc" -eq 137 ] && [ $(( $(date +%s 2>/dev/null || echo 0) - _start )) -ge "${_duration%%.*}" ]; then
|
||||
_rc=124
|
||||
fi
|
||||
return "$_rc"
|
||||
else
|
||||
# Stock macOS ships neither coreutils gtimeout nor timeout(1); running
|
||||
# unwrapped let a hung `codex exec` block the probe — and the calling
|
||||
# workflow — indefinitely. Emulate: background the command, TERM it at
|
||||
# the deadline, mirror timeout(1)'s exit-124 contract. The watchdog's
|
||||
# stdout is detached so an early finish never blocks a caller's $(...)
|
||||
# capture on the orphaned sleep.
|
||||
# the deadline, KILL it after the grace period if it is still alive, and
|
||||
# mirror timeout(1)'s exit-124 contract. The watchdog's stdout is
|
||||
# detached so an early finish never blocks a caller's $(...) capture on
|
||||
# the orphaned sleep.
|
||||
"$@" &
|
||||
local _cmd_pid=$!
|
||||
( sleep "$_duration" && trap '' TERM && kill -TERM "$_cmd_pid" 2>/dev/null && exit 124 ) >/dev/null 2>&1 &
|
||||
(
|
||||
sleep "$_duration" && trap '' TERM && kill -TERM "$_cmd_pid" 2>/dev/null || exit 0
|
||||
_GSTACK_CODEX_WAITED=0
|
||||
while [ "$_GSTACK_CODEX_WAITED" -lt "$_grace" ] && kill -0 "$_cmd_pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
_GSTACK_CODEX_WAITED=$((_GSTACK_CODEX_WAITED + 1))
|
||||
done
|
||||
pkill -KILL -P "$_cmd_pid" 2>/dev/null
|
||||
kill -KILL "$_cmd_pid" 2>/dev/null
|
||||
exit 124
|
||||
) >/dev/null 2>&1 &
|
||||
local _watch_pid=$!
|
||||
local _rc
|
||||
wait "$_cmd_pid"
|
||||
_rc=$?
|
||||
kill "$_watch_pid" 2>/dev/null
|
||||
@@ -244,6 +316,6 @@ _gstack_codex_log_hang() {
|
||||
local _log_bin="$HOME/.claude/skills/gstack/bin/gstack-learnings-log"
|
||||
[ -x "$_log_bin" ] || return 0
|
||||
local _key="codex-hang-$(date +%s 2>/dev/null || echo unknown)"
|
||||
"$_log_bin" "$(printf '{"skill":"codex","type":"operational","key":"%s","insight":"Codex timed out after 600s 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")" \
|
||||
"$_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
|
||||
}
|
||||
Reference in new issue
Block a user