Files
gstack/bin/gstack-doctor
T
4dfd83bf8c v1.91.60.0 fix(windows): setup degrades instead of dying on a CSO failure, detects Smart App Control, Consolas terminal font (#3079)
* Fix spaced-out terminal text on Windows: put a Latin monospace before the CJK fallbacks

On Windows none of the Mac/Linux monospace fonts in the stack are
installed, so the first font that resolves is Malgun Gothic (the Windows
Korean UI font). xterm.js sizes its character cell from that font's
advance width, and Malgun Gothic advances Latin glyphs in a full-width
CJK cell — doubling every cell and rendering the whole terminal (and the
sidepanel mono UI) as spaced-out text.

Insert Consolas (present on every Windows install) and the Cascadia
faces ahead of the CJK fonts in both the xterm fontFamily and the
--font-mono CSS var. The CJK fonts stay last for actual CJK coverage.
No bundled font, no user install needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKj8Nr8T4GvryTcWBJRMyC

* fix(windows): every compiled binary answers --version before any side effect

browse --version started a server and exited 1, pdf printed usage and
exited 1, gstack-global-discover rejected the flag, and find-browse ran
discovery. A launch probe for Smart App Control (#2595) needs one
side-effect-free command that exits 0 on a healthy build, so browse,
find-browse, make-pdf and gstack-global-discover now answer --version
first (design already did).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(extension): pin Consolas ahead of the CJK fallbacks in both mono stacks (#2287)

Regression test for @tomfluff's fix: the xterm fontFamily and --font-mono
must list Consolas before Noto Sans Mono CJK KR and Malgun Gothic, or
Windows renders the sidebar terminal double-width.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(cso): record each CSO build's outcome and stop arming the parent trap across the native publisher exec (#3071)

scripts/build-cso.sh now writes bin/.gstack-cso-build-result: result,
failed stage (build or publish), reason, this revision, the revision of
the generation still installed, whether its launcher is usable, and the
diagnostic log. An 'interrupted' entry is written before each stage so a
killed run is visible. A failed publish that restores the old generation
records it as still installed.

Before exec'ing the native publisher, the parent re-checks the locker,
clears its EXIT trap and enables execfail: the locked child's own trap
owns the stage. On Git Bash an exec of a native program may not replace
the shell, so the parent's trap could otherwise delete the stage under
the running child (the vanishing files in #3071; root cause unproven).
If the exec itself fails the parent cleans the stage and records it.

The Windows build prefers PowerShell 7 (pwsh) over Windows PowerShell
5.1, which crashed outright on the reporter's machine.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(setup): an optional CSO build or publish failure no longer aborts setup (#3071)

CSO was optional at probe time but fatal at build time: scripts/build.sh
ran build-cso.sh under set -e before the browse server bundle and the
completion stamp, and setup exited when the CSO artifact set was
incomplete. A failed CSO step now prints 'CSO unavailable: the <stage>
step failed' with its log (bin/.gstack-cso-build.log) and retry, and the
build and setup finish. GSTACK_STRICT_BUILD=1 keeps it fatal; the free-tests,
make-pdf-gate and windows-setup-e2e builds set it, so regressions still
fail CI.

setup's summary reads the build-result record instead of the pre-build
availability flag: probe failures keep their prerequisite instruction;
build and publish failures name the stage, the diagnostic and
'bun run build:cso && ./setup', and a restored earlier helper is
reported as 'previous CSO kept: <revision>'. setup records probe
failures in the same file.

The Windows prerequisite probe prefers pwsh, and a probe that fails for
any reason other than a missing toolchain or a compile error keeps its
exit status and first output line (windows-toolchain-probe) instead of
being labeled windows-msvc-toolchain. Reported and diagnosed by
@dviolante.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(windows): detect Smart App Control-blocked binaries in setup and gstack-doctor (#2595, #2124)

Smart App Control refuses the five unsigned binaries gstack compiles
locally, and Git Bash reports it as 'Permission denied', so setup's
[ -x ] gate passed and skills failed later. bin/gstack-launch-probe.sh
runs each binary's side-effect-free --version and classifies it:
native, blocked (spawn error, exit 126/127, or an application-control
message), broken (launched, exited non-zero) or missing. It shows the
raw first output line, since the exact Windows text is unverified.

On Windows, setup names each blocked binary and the skills that need it,
cites #2595, says which shell ran setup and whether WSL is installed,
gives today's workarounds (WSL, or turning Smart App Control off), how to
undo, and the new troubleshooting section. gstack-doctor's browse row now
says 'blocked at launch' instead of passing, a binaries row covers the
other four, and a cso row reads the build-result record.

windows-setup-e2e builds with GSTACK_STRICT_BUILD=1, checks all five
binaries answer --version, and adds two forced CSO failures: fatal under
strict mode, and a completed setup that reports the failure and a doctor
cso warning under GSTACK_STRICT_BUILD=0.

No shim or launcher: it could not be called from PowerShell and would
load the caller's project .env/bunfig. Signing is the follow-up in
TODOS.md. Diagnosis credit: @knetics9000 (#2595, #2596), @smartjelic-sys
(#2595), @pstilwell90 (#2124), @cko32002 (#2265), @salluexez (#2127).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: Windows Smart App Control known issue, optional CSO failures, doctor rows

README's Windows section says /cso failures no longer stop setup
(GSTACK_STRICT_BUILD=1 keeps them fatal), documents Smart App Control as
a known issue with today's workarounds (WSL, or turning it off) and the
Consolas fix. troubleshooting.md gains a Windows section for blocked
binaries and an entry for CSO build/publish failures. TODOS.md replaces
the Node CLI lane item with Windows code signing (needs a signing
account), the #3071 root-cause follow-up and SAC text verification.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(setup): never write the CSO build record into a source checkout that needed no build

Setup wrote bin/.gstack-cso-build-result for every probe-stage outcome,
so a run that built nothing still changed the source checkout's bin/
(caught by setup-codex-scope's unchanged-source checks). A missing
prerequisite now comes from the run's own probe, as before, and setup
only removes a stale record alongside the removed helper.
scripts/build-cso.sh remains the only writer. The doctor's cso row
reports 'native helper not built' with no record.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(extension): move the Consolas font-stack check into sidebar-ux (#2287)

The standalone file read extension sources without importing product
code, which the test-of-test ratchet rejects; sidebar-ux already owns
the sidepanel.css structural checks.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.43.0 fix(windows): setup degrades instead of dying on a CSO failure, detects Smart App Control, Consolas terminal font

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Yotam <yotsurfing@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-08 15:43:32 +00:00

457 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-doctor — readiness report without starting a skill.
#
# Usage:
# gstack-doctor print one row per check; spends nothing
# gstack-doctor --live also run the paid Codex model probe (one short call)
#
# Rows: install root, state root, bun, hooks, codex, codex probe, artifacts
# sync, browse bundle, binaries, cso, claude code, session journal, guard log. Each row is
# `ok`, `warn`, `not configured` or `fail`; a row that is not ok carries a fix
# line, and every gstack helper in a fix line is an absolute path under this
# install root. Exit 1 when any row is `fail`, 2 on a usage error, else 0.
#
# Read-only. Each row reuses the owner's check instead of new probe logic: the
# install registry rows behind `./setup --status`, bin/gstack-bun-version.sh,
# bin/gstack-hook-check, bin/gstack-codex-probe's subcommands (the paid
# `probe-model` only with --live; otherwise the cached result and its age),
# the status file bin/gstack-brain-sync writes, setup's browse build
# outputs, setup's launch probe (bin/gstack-launch-probe.sh, which runs each
# compiled binary's side-effect-free --version) and the CSO build-result record. The sync row prints only a clamped status code and counts, never
# the status message or file paths: the message can quote a matched secret.
# Bash 3.2 plus POSIX tools; it still reports when bun or python3 is missing.
# Heredoc delivery guard, as in bin/gstack-brain-sync: compat level 50 keeps
# heredoc bodies on the tempfile path, so a small pipe buffer cannot hang them.
BASH_COMPAT=50
set -u
usage() {
sed -n '3,6p' "$0" | sed 's/^# \{0,1\}//'
}
LIVE=0
for _arg in "$@"; do
case "$_arg" in
--live) LIVE=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "gstack-doctor: unknown argument '$_arg'" >&2; usage >&2; exit 2 ;;
esac
done
ROOT="$(cd "$(dirname "$0")/.." && pwd)" || { echo "gstack-doctor: cannot resolve the install root" >&2; exit 1; }
ROOT_REAL="$(cd "$ROOT" && pwd -P)"
BIN="$ROOT/bin"
SRC="$ROOT_REAL"
DOCS="https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md"
CODEX_FEATURES="outside reviews in /review, /ship, /autoplan and /codex"
N_OK=0 N_WARN=0 N_NC=0 N_FAIL=0
row() {
printf '%-15s %-15s %s\n' "$1" "$2" "$3"
[ -n "${4:-}" ] && printf '%32sfix: %s\n' '' "$4"
case "$1" in
ok) N_OK=$((N_OK + 1)) ;;
warn) N_WARN=$((N_WARN + 1)) ;;
'not configured') N_NC=$((N_NC + 1)) ;;
*) N_FAIL=$((N_FAIL + 1)) ;;
esac
}
# clamp CHARSET MAX — stdin's first line reduced to CHARSET, at most MAX chars.
clamp() { head -1 | tr -cd "$1" | cut -c1-"$2"; }
# epoch_of VALUE — epoch seconds for an epoch or an ISO-8601 UTC time; "" otherwise.
epoch_of() {
local v
v=$(printf '%s' "$1" | tr -cd '0-9TZ:.+-')
case "$v" in
'') return 0 ;;
*[!0-9]*) date -u -d "$v" +%s 2>/dev/null || date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$v" +%s 2>/dev/null || true ;;
*) printf '%s' "$v" ;;
esac
}
# json_field KEY JSON — a top-level string or number value from one-line JSON.
# A key quoted inside a string value is escaped (\"key\"), so it never matches.
json_field() {
local v
v=$(printf '%s' "$2" | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"\\\\]*\)\".*/\1/p")
[ -n "$v" ] || v=$(printf '%s' "$2" | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p")
printf '%s' "$v"
}
check_state_root() {
if [ -d "$STATE_ROOT" ]; then
row ok "state root" "$STATE_ROOT (selected by $_gstack_sr_var)"
else
row warn "state root" "$STATE_ROOT (selected by $_gstack_sr_var) does not exist yet" "cd $SRC && ./setup, or run any gstack skill"
fi
}
check_install() {
local rows host scope src dest reg cur result note real installed="" bad="" badnote="" other="" total=0
if ! . "$BIN/gstack-install-registry.sh" 2>/dev/null; then
row fail "install root" "$BIN/gstack-install-registry.sh is missing" "reinstall gstack from a fresh checkout of https://github.com/garrytan/gstack, then run its ./setup"
return
fi
rows="$(_gstack_status_rows)"
while IFS=' ' read -r host scope src dest reg cur result note; do
[ -n "$host" ] || continue
total=$((total + 1))
real="$(cd "$src" 2>/dev/null && pwd -P)"
if [ "$real" != "$ROOT_REAL" ] && [ "$(cd "$dest/gstack" 2>/dev/null && pwd -P)" != "$ROOT_REAL" ]; then
[ -n "$other" ] || other="$src"
continue
fi
[ -n "$real" ] && SRC="$real"
case "$result" in
current) installed="${installed:+$installed, }$host $scope current ($cur)" ;;
*) bad="${bad:+$bad, }$host $scope $result"; [ -n "$badnote" ] || badnote="$note" ;;
esac
done <<ROWS
$rows
ROWS
if [ -n "$bad" ]; then
row warn "install root" "$ROOT: $bad${installed:+; $installed}" "${badnote:-cd $SRC && ./setup}"
elif [ -n "$installed" ]; then
row ok "install root" "$ROOT: $installed"
elif [ "$total" -eq 0 ]; then
row fail "install root" "$ROOT: no gstack install is registered" "cd $SRC && ./setup"
else
row warn "install root" "$ROOT is not the source of any of the $total registered install(s); this doctor checks this checkout" "cd $SRC && ./setup, or run $other/bin/gstack-doctor"
fi
}
check_bun() {
local found where
if ! . "$BIN/gstack-bun-version.sh" 2>/dev/null; then
row fail bun "$BIN/gstack-bun-version.sh is missing" "reinstall gstack from a fresh checkout of https://github.com/garrytan/gstack"
return
fi
if ! where=$(command -v bun 2>/dev/null); then
row fail bun "not found on PATH; gstack's skills, hooks and builds need it" "install Bun $GSTACK_BUN_TESTED (https://bun.sh), then cd $SRC && ./setup"
return
fi
found=$(bun --version </dev/null 2>/dev/null | clamp 'A-Za-z0-9.+-' 40)
case "$(gstack_bun_status "$found")" in
ok) row ok bun "$found at $where (floor $GSTACK_BUN_FLOOR, tested $GSTACK_BUN_TESTED)" ;;
untested) row warn bun "$found at $where is below the tested $GSTACK_BUN_TESTED" "bun upgrade ($DOCS#bun-too-old)" ;;
too-old) row fail bun "$found at $where is below the security floor $GSTACK_BUN_FLOOR" "bun upgrade, then cd $SRC && ./setup ($DOCS#bun-too-old)" ;;
*) row warn bun "could not read the version of $where" "bun upgrade ($DOCS#bun-too-old)" ;;
esac
}
check_hooks() {
local out rc checked missing first
out=$("$BIN/gstack-hook-check" "$SRC" </dev/null 2>&1)
rc=$?
checked=$(printf '%s\n' "$out" | grep -c '^ok ')
missing=$(printf '%s\n' "$out" | grep -c '^missing ')
case "$rc" in
0) if [ "$missing" -gt 0 ]; then row ok hooks "$checked hook(s) parse; $missing not installed"; else row ok hooks "$checked hook(s) parse"; fi ;;
1) first=$(printf '%s\n' "$out" | grep '^fail ' | head -1 | cut -c6-300)
row fail hooks "$first" "repair the file (git -C $SRC status shows half-applied edits), then cd $SRC && ./setup ($DOCS#setup-hook-does-not-parse)" ;;
3) row warn hooks "TypeScript hook entries not checked: bun not found" "install Bun (see the bun row), then rerun $BIN/gstack-doctor" ;;
*) row fail hooks "the hook check could not run (exit $rc)" "cd $SRC && ./setup" ;;
esac
}
# check_codex — CODEX_MODE from the free checks; sets CODEX_READY=1 when the
# probe row should follow.
check_codex() {
local reviews probe="$BIN/gstack-codex-probe" sel rc model src
CODEX_READY=0
reviews=$("$BIN/gstack-config" get codex_reviews 2>/dev/null || echo enabled)
if [ "$reviews" = disabled ]; then
row "not configured" codex "codex_reviews disabled; $CODEX_FEATURES skip Codex" "$BIN/gstack-config set codex_reviews enabled"
return
fi
if ! command -v codex >/dev/null 2>&1; then
row "not configured" codex "Codex CLI not installed; $CODEX_FEATURES have no Codex coverage" "npm install -g @openai/codex, then codex login"
return
fi
sel=$("$probe" select-model exec </dev/null 2>&1)
rc=$?
model=$(printf '%s\n' "$sel" | sed -n 's/^CODEX_SEL: //p' | clamp 'A-Za-z0-9._:/-' 80)
if [ "$rc" -ne 0 ] || [ -z "$model" ]; then
[ "$rc" -ne 0 ] && rc="exited $rc" || rc="printed no CODEX_SEL line"
row fail codex "CODEX_MODE helper_unavailable: select-model $rc (the probe could not locate its scripts, or the requested model id is invalid)" "run $probe select-model exec to see the cause; reinstall with cd $SRC && ./setup"
return
fi
src=$(printf '%s\n' "$sel" | sed -n 's/^CODEX_MODEL: .*source: \(.*\))$/\1/p' | clamp 'A-Za-z0-9 ._:/=-' 80)
gstack_codex_version
if ! "$probe" check-auth </dev/null >/dev/null 2>&1; then
row warn codex "CODEX_MODE not_authed; model $model (source: ${src:-unknown})" "codex login, or export CODEX_API_KEY"
return
fi
if ! "$probe" check-sandbox </dev/null >/dev/null 2>&1; then
row warn codex "CODEX_MODE sandbox_unavailable; no paid call runs" "run $probe check-sandbox for the reason and fix"
return
fi
if "$probe" check-version </dev/null 2>&1 | grep -q '^WARN'; then
row warn codex "${_gcx_version:-version unknown} has a known problem; model $model (source: ${src:-unknown})" "run $probe check-version for the fix"
else
row ok codex "${_gcx_version:-version unknown}; self-locate ok; model $model (source: ${src:-unknown})"
fi
CODEX_READY=1
}
check_codex_probe() {
local out rc state probe="$BIN/gstack-codex-probe" model_fix="set GSTACK_CODEX_MODEL=<supported-model> or model in config.toml, then $BIN/gstack-doctor --live"
if [ "$LIVE" -eq 1 ]; then
out=$("$probe" probe-model exec </dev/null 2>&1)
rc=$?
state=$(printf '%s\n' "$out" | sed -n 's/^CODEX_PROBE_STATE: //p' | clamp 'a-z_' 40)
case "$rc" in
0) case "$state" in
inconclusive|rate_limited) row warn "codex probe" "live probe: CODEX_MODE unverified ($state)" "rerun $BIN/gstack-doctor --live later" ;;
*) row ok "codex probe" "live probe: ok" ;;
esac ;;
1) row warn "codex probe" "live probe: model_unusable; $CODEX_FEATURES skip Codex" "$model_fix" ;;
2) row fail "codex probe" "live probe: broken_install (the Codex CLI cannot execute)" "npm install -g @openai/codex" ;;
4) row warn "codex probe" "live probe: quota_exhausted (the account's Codex usage limit)" "wait for the reset time Codex names: $probe probe-model exec" ;;
*) row warn "codex probe" "live probe: exit $rc${state:+ ($state)}" "rerun $BIN/gstack-doctor --live" ;;
esac
return
fi
gstack_codex_cache_read "$STATE_ROOT"
local age
age=$(gstack_codex_age_text "$_gcx_age")
case "$_gcx_probe" in
'') row warn "codex probe" "not probed; Codex readiness is unverified until a skill or --live runs it" "$BIN/gstack-doctor --live (one short paid Codex call)" ;;
MODEL_OK) row ok "codex probe" "cached MODEL_OK, $age (not re-run; --live probes now)" ;;
MODEL_UNUSABLE) row warn "codex probe" "cached MODEL_UNUSABLE, $age; $CODEX_FEATURES skip Codex" "$model_fix" ;;
MODEL_QUOTA_EXHAUSTED) row warn "codex probe" "cached MODEL_QUOTA_EXHAUSTED, $age (the account's Codex usage limit)" "wait for the reset, then $BIN/gstack-doctor --live" ;;
*) row warn "codex probe" "cached entry unrecognized, $age" "$BIN/gstack-doctor --live" ;;
esac
}
check_sync() {
local mode file json status held drainable drain push queue=0 now f detail fix="$BIN/gstack-brain-sync --status"
mode=$("$BIN/gstack-config" get artifacts_sync_mode 2>/dev/null || echo off)
if [ ! -d "$STATE_ROOT/.git" ]; then
row "not configured" "artifacts sync" "off; cross-machine artifacts sync is unavailable" "$BIN/gstack-artifacts-init"
return
fi
if [ "$mode" = off ]; then
row "not configured" "artifacts sync" "artifacts_sync_mode off; cross-machine artifacts sync is unavailable" "$BIN/gstack-config set artifacts_sync_mode artifacts-only"
return
fi
for f in "$STATE_ROOT/.brain-queue.d"/*.json; do [ -f "$f" ] && queue=$((queue + 1)); done
for f in "$STATE_ROOT/.brain-queue.jsonl" "$STATE_ROOT/.brain-queue.jsonl.migrating"; do
[ -f "$f" ] && queue=$((queue + $(wc -l < "$f" | tr -d ' ')))
done
file="$STATE_ROOT/.brain-sync-status.json"
if [ ! -f "$file" ]; then
if [ "$queue" -gt 0 ]; then row warn "artifacts sync" "no drain recorded while $queue record(s) are queued" "$fix"
else row ok "artifacts sync" "mode $mode; no drain yet, queue empty"; fi
return
fi
json=$(head -c 16384 "$file" 2>/dev/null | tr '\n\r' ' ')
status=$(json_field status "$json")
case "$status" in ok|idle|held|blocked|push_failed|error) ;; *) status=unknown ;; esac
held=$(json_field held_count "$json" | tr -cd '0-9')
drainable=$(json_field drainable "$json" | tr -cd '0-9')
drain=$(epoch_of "$(json_field last_drain_at "$json")")
push=$(epoch_of "$(json_field last_push_at "$json")")
[ -n "$push" ] || push=$(epoch_of "$(head -1 "$STATE_ROOT/.brain-last-push" 2>/dev/null)")
now=$(date +%s)
case "$status" in
held) row warn "artifacts sync" "status held${held:+ ($held file(s) held by the secret scanner)}" "$fix"; return ;;
blocked) row warn "artifacts sync" "status blocked (a re-scan still flags staged content)" "$fix"; return ;;
push_failed|error|unknown) row warn "artifacts sync" "status $status" "$fix"; return ;;
esac
if [ "${drainable:-0}" -gt 0 ] && { [ -z "$push" ] || [ $((now - push)) -gt 86400 ]; }; then
row warn "artifacts sync" "status $status; no push for 24h while $drainable record(s) wait" "$fix"
elif [ "$queue" -gt 0 ] && [ -n "$drain" ] && [ $((now - drain)) -gt 86400 ]; then
row warn "artifacts sync" "status $status; no drain for 24h while $queue record(s) are queued" "$fix"
else
detail="status $status (mode $mode; queue $queue)"
row ok "artifacts sync" "$detail"
fi
}
# launch_fix — the fix line for a binary the OS refuses to start.
launch_fix() {
if [ -n "$EXE" ]; then
echo "Windows Smart App Control (or another application-control policy) blocks unsigned local builds (#2595): run gstack inside WSL or turn Smart App Control off, then cd $SRC && ./setup ($DOCS#windows-smart-app-control)"
else
echo "the OS refused to execute it (permissions or a noexec mount under $ROOT); fix that, then cd $SRC && ./setup"
fi
}
check_browse() {
local missing="" f
if [ ! -x "$ROOT/browse/dist/browse$EXE" ]; then
row warn "browse bundle" "not built; the gstack browser fallback for /browse, /qa, /qa-only, /design-review, /make-pdf, /pair-agent is unavailable (the Aside browser is unaffected)" "cd $SRC && ./setup"
return
fi
gstack_launch_probe "$ROOT/browse/dist/browse$EXE"
case "$_glp_state" in
blocked) row warn "browse bundle" "built, but blocked at launch (${_glp_detail:-no output}); the gstack browser fallback is unavailable (the Aside browser is unaffected)" "$(launch_fix)"; return ;;
broken) row warn "browse bundle" "built, but browse --version failed (${_glp_detail})" "cd $SRC && bun run build"; return ;;
esac
for f in server-node.mjs .build-complete; do [ -e "$ROOT/browse/dist/$f" ] || missing="${missing:+$missing, }$f"; done
if [ -n "$missing" ]; then
row warn "browse bundle" "incomplete build (missing browse/dist: $missing)" "cd $SRC && ./setup"
else
row ok "browse bundle" "browse/dist built"
fi
}
# check_binaries — the other compiled binaries, by launch state (browse has its own row).
check_binaries() {
local rel detail blocked="" broken="" missing="" skills="" n=0
for rel in browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover; do
gstack_launch_probe "$ROOT/$rel$EXE"
detail="${rel##*/}"
case "$_glp_state" in
native) n=$((n + 1)) ;;
blocked) blocked="${blocked:+$blocked, }$detail (${_glp_detail:-no output})"; skills="${skills:+$skills, }$(gstack_launch_probe_skills "$rel")" ;;
broken) broken="${broken:+$broken, }$detail (${_glp_detail})" ;;
missing) missing="${missing:+$missing, }$detail" ;;
esac
done
if [ -n "$blocked" ]; then
row warn binaries "blocked at launch: $blocked; unavailable: $skills" "$(launch_fix)"
elif [ -n "$broken" ]; then
row warn binaries "launched but failed: $broken" "cd $SRC && bun run build"
elif [ -n "$missing" ]; then
row warn binaries "not built: $missing" "cd $SRC && ./setup"
else
row ok binaries "find-browse, design, pdf, gstack-global-discover launch ($n of 4)"
fi
}
# check_cso — from the build-result record setup and scripts/build-cso.sh write.
check_cso() {
local file="$SRC/bin/.gstack-cso-build-result" result stage reason installed diag usable=0
[ -x "$SRC/bin/gstack-cso-launcher$EXE" ] && [ -x "$SRC/bin/gstack-cso-core$EXE" ] && usable=1
cso_field() { sed -n "s/^$1=//p" "$file" 2>/dev/null | head -1 | clamp ' -~' 200; }
result=$(cso_field result) stage=$(cso_field stage) reason=$(cso_field reason) installed=$(cso_field installed) diag=$(cso_field diagnostic)
case "$result" in
ok) if [ "$usable" -eq 1 ]; then row ok cso "native helper $installed"
else row warn cso "the last build succeeded but the native helper is missing; /cso reports not assessed" "cd $SRC && ./setup"; fi ;;
failed)
if [ "$usable" -eq 1 ] && [ -n "$installed" ]; then
row warn cso "$stage step failed ($reason) for $(cso_field revision); previous CSO kept: $installed" "cd $SRC && bun run build:cso && ./setup (log: ${diag:-none}; $DOCS#cso-build-or-publish-failed)"
else
row warn cso "unavailable: the $stage step failed ($reason); /cso reports not assessed" "cd $SRC && bun run build:cso && ./setup (log: ${diag:-none}; $DOCS#cso-build-or-publish-failed)"
fi ;;
*) if [ "$usable" -eq 1 ]; then row ok cso "native helper built (no build record yet)"
else row "not configured" cso "native helper not built; /cso reports not assessed" "cd $SRC && ./setup (it names any missing build prerequisite)"; fi ;;
esac
}
check_claude() {
local override cmd raw ver rc a
local args=()
override="${GSTACK_CLAUDE_BIN:-${CLAUDE_BIN:-}}"
override="${override#\"}"; override="${override%\"}"
if [ -n "$override" ]; then
case "$override" in /*|[A-Za-z]:*) cmd="$override" ;; *) cmd=$(command -v "$override" 2>/dev/null) ;; esac
raw="${GSTACK_CLAUDE_BIN_ARGS:-${CLAUDE_BIN_ARGS:-}}"
case "$raw" in
'') ;;
'['*) raw="${raw#[}"; raw="${raw%]}"
while IFS= read -r a; do
a=$(printf '%s' "$a" | sed 's/^[[:space:]]*"\{0,1\}//; s/"\{0,1\}[[:space:]]*$//')
[ -n "$a" ] && args+=("$a")
done <<ARGS
$(printf '%s' "$raw" | tr ',' '\n')
ARGS
;;
*) raw="${raw#\"}"; args+=("${raw%\"}") ;;
esac
else
cmd=$(command -v claude 2>/dev/null)
fi
if [ -z "${cmd:-}" ]; then
row "not configured" "claude code" "Claude Code CLI not found; the Claude Code host and Claude outside reviews are unavailable" "install Claude Code (https://code.claude.com), or set GSTACK_CLAUDE_BIN"
return
fi
ver=$("$cmd" ${args[@]+"${args[@]}"} --version </dev/null 2>/dev/null)
rc=$?
ver=$(printf '%s\n' "$ver" | clamp 'A-Za-z0-9 ._()+-' 60)
if [ "$rc" -ne 0 ]; then
row warn "claude code" "$cmd --version failed (exit $rc)" "reinstall Claude Code (https://code.claude.com)"
else
row ok "claude code" "${ver:-version unknown} at $cmd"
fi
}
check_journal() {
local cwd dir f size largest=0 count=0
case "${OSTYPE:-}" in msys*|cygwin*) cwd=$(pwd -W 2>/dev/null || pwd -P) ;; *) cwd=$(pwd -P) ;; esac
dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/$(printf '%s' "$cwd" | sed 's/[^A-Za-z0-9]/-/g')"
for f in "$dir"/*.jsonl; do
[ -f "$f" ] || continue
count=$((count + 1))
size=$(wc -c < "$f" | tr -d ' ')
[ "$size" -gt "$largest" ] && largest="$size"
done
if [ "$count" -eq 0 ]; then
row ok "session journal" "no session journals for this project"
return
fi
row ok "session journal" "largest of $count: $(awk -v b="$largest" 'BEGIN { if (b >= 1048576) printf "%.1f MiB", b / 1048576; else if (b >= 1024) printf "%.1f KiB", b / 1024; else printf "%d B", b }')"
}
check_guard_log() {
local log="$STATE_ROOT/analytics/autoplan-guard.jsonl" summary
if [ ! -f "$log" ]; then
row ok "guard log" "no guard decisions logged"
return
fi
summary=$(awk '
!/^\{/ || !match($0, /"schema"[[:space:]]*:[[:space:]]*1[,}[:space:]]/) { next }
match($0, /"decision"[[:space:]]*:[[:space:]]*"(allow|deny)"/) {
n++; if (substr($0, RSTART, RLENGTH) ~ /deny/) d++
}
match($0, /"reason_code"[[:space:]]*:[[:space:]]*"[a-z0-9_]+"/) {
c = substr($0, RSTART, RLENGTH); sub(/^[^:]*:[[:space:]]*"/, "", c); sub(/"$/, "", c)
codes[k++ % 5] = c
}
END {
out = (n + 0) " decisions (" (d + 0) " denied)"
if (k > 0) {
out = out "; last codes:"; sep = " "
for (i = (k > 5 ? k - 5 : 0); i < k; i++) { out = out sep codes[i % 5]; sep = ", " }
}
print out
}' "$log" 2>/dev/null)
row ok "guard log" "${summary:-0 decisions (0 denied)}"
}
if ! . "$BIN/gstack-state-root.sh" 2>/dev/null; then
row fail "state root" "$BIN/gstack-state-root.sh is missing" "reinstall gstack from a fresh checkout of https://github.com/garrytan/gstack, then run its ./setup"
exit 1
fi
gstack_state_root_select
STATE_ROOT="$_gstack_sr_root"
GSTACK_STATE_ROOT="$STATE_ROOT"
. "$BIN/gstack-codex-status.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-codex-status.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
. "$BIN/gstack-launch-probe.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-launch-probe.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
EXE=""
case "${OSTYPE:-}" in msys*|cygwin*|win32*) EXE=".exe" ;; esac
echo "gstack doctor $(head -1 "$ROOT/VERSION" 2>/dev/null | clamp '0-9A-Za-z.+-' 40)"
check_install
check_state_root
check_bun
check_hooks
check_codex
[ "$CODEX_READY" -eq 1 ] && check_codex_probe
check_sync
check_browse
check_binaries
check_cso
check_claude
check_journal
check_guard_log
echo "$N_OK ok, $N_WARN warn, $N_NC not configured, $N_FAIL fail"
[ "$N_FAIL" -eq 0 ]