mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* 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>
457 lines
21 KiB
Bash
Executable File
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 ]
|