mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 07:47:27 +01:00
feat(doctor): --min-revision, a launch Bun found off PATH, planning-only pins SKIP
gstack-doctor --check gains --min-revision <v>|unknown: below it the revision row FAILs with REVISION_BELOW_MINIMUM naming the Capy wrapper or bin/gstack-upgrade; unknown says so and never claims the minimum. The runtime and pins rows name a supported Bun found in $BUN_INSTALL/bin (not on PATH) with the export/GSTACK_LAUNCH fix (gstack_bun_launch_dir, one helper in bin/gstack-bun-version.sh). --for a planning-only workflow (autoplan, the plan reviews, office-hours) reports a repo test-pin mismatch as SKIP pins (planning-only; ...). New result codes UPGRADE_REFUSED, UPGRADE_FETCH_FAILED, REVISION_BELOW_MINIMUM, CAPY_WRAPPER_STALE, INSTALL_UNVERIFIED with troubleshooting anchors; REVISION_UNMET's fix names bin/gstack-upgrade. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
b4481089ef
commit
5c00c10e69
8 files changed
+304
-15
No files matched your search
@@ -88,3 +88,29 @@ gstack_bun_incoming_hold() {
|
||||
_gb_ver=$(git -C "$1" show "$2:VERSION" 2>/dev/null | head -1)
|
||||
echo "bun-too-old: found Bun $_gb_found at $(command -v bun); gstack ${_gb_ver:-update} needs $_gb_floor or newer"
|
||||
}
|
||||
|
||||
# gstack_bun_launch_dir [exact] — the directory to put first on PATH so
|
||||
# gstack's `#!/usr/bin/env bun` bins run a supported Bun: the first of
|
||||
# $BUN_INSTALL/bin and $HOME/.bun/bin whose bun is ok (and equals <exact>
|
||||
# when given), unless the bun already first on PATH qualifies. Prints the
|
||||
# directory (callers read its version with "<dir>/bun --version"); prints
|
||||
# nothing and returns 1 when PATH's bun suffices or no candidate qualifies. Shared by
|
||||
# gstack-capy-install (the GSTACK_LAUNCH line) and gstack-doctor --check (the
|
||||
# runtime row's `found at ... (not on PATH)`).
|
||||
gstack_bun_launch_dir() {
|
||||
_gb_path_bun=$(command -v bun 2>/dev/null || true)
|
||||
if [ -n "$_gb_path_bun" ]; then
|
||||
_gb_v=$(bun --version </dev/null 2>/dev/null | head -1)
|
||||
if [ "$(gstack_bun_status "$_gb_v")" = ok ] && { [ -z "${1:-}" ] || [ "$_gb_v" = "$1" ]; }; then return 1; fi
|
||||
fi
|
||||
for _gb_dir in "${BUN_INSTALL:+$BUN_INSTALL/bin}" "${HOME:+$HOME/.bun/bin}"; do
|
||||
[ -n "$_gb_dir" ] && [ -x "$_gb_dir/bun" ] || continue
|
||||
[ "$_gb_dir/bun" = "$_gb_path_bun" ] && continue
|
||||
_gb_v=$("$_gb_dir/bun" --version </dev/null 2>/dev/null | head -1)
|
||||
[ "$(gstack_bun_status "$_gb_v")" = ok ] || continue
|
||||
[ -z "${1:-}" ] || [ "$_gb_v" = "$1" ] || continue
|
||||
printf '%s\n' "$_gb_dir"
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
+4
-2
@@ -5,7 +5,7 @@
|
||||
# gstack-doctor print one row per check; spends nothing
|
||||
# gstack-doctor --live also run the paid Codex model probe (one short call)
|
||||
# gstack-doctor --check [--for <skill> | --require <id,id>] [--project <path>]
|
||||
# [--revision <ref>] [--json] [--live]
|
||||
# [--revision <ref>] [--min-revision <v>|unknown] [--json] [--live]
|
||||
# one PASS | FAIL | SKIP row per component, then one
|
||||
# trailer: `gstack: ok <revision> for=<what>` (exit 0)
|
||||
# or `gstack: fail <id,id>` (exit 1). --for names the
|
||||
@@ -39,7 +39,7 @@ usage() {
|
||||
sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
LIVE=0 CHECK=0 CHECK_JSON=0 CHECK_FOR="" CHECK_REQUIRE="" CHECK_PROJECT="" CHECK_REVISION=""
|
||||
LIVE=0 CHECK=0 CHECK_JSON=0 CHECK_FOR="" CHECK_REQUIRE="" CHECK_PROJECT="" CHECK_REVISION="" CHECK_MIN_REVISION=""
|
||||
_need_value() { [ -n "${2:-}" ] || { echo "gstack-doctor: $1 needs a value" >&2; usage >&2; exit 2; }; }
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -54,6 +54,8 @@ while [ $# -gt 0 ]; do
|
||||
--project=*) CHECK=1; CHECK_PROJECT="${1#--project=}" ;;
|
||||
--revision) _need_value "$1" "${2:-}"; CHECK=1; CHECK_REVISION="$2"; shift ;;
|
||||
--revision=*) CHECK=1; CHECK_REVISION="${1#--revision=}" ;;
|
||||
--min-revision) _need_value "$1" "${2:-}"; CHECK=1; CHECK_MIN_REVISION="$2"; shift ;;
|
||||
--min-revision=*) CHECK=1; CHECK_MIN_REVISION="${1#--min-revision=}" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "gstack-doctor: unknown argument '$1'" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
+73
-11
@@ -14,7 +14,10 @@
|
||||
# Codex model probe runs only with --live.
|
||||
#
|
||||
# Inputs (set by bin/gstack-doctor): CHECK_FOR (skill), CHECK_REQUIRE (csv),
|
||||
# CHECK_JSON (0|1), CHECK_PROJECT (path or ""), CHECK_REVISION (ref or ""), LIVE.
|
||||
# CHECK_JSON (0|1), CHECK_PROJECT (path or ""), CHECK_REVISION (ref or ""),
|
||||
# CHECK_MIN_REVISION (the supported minimum the Capy wrapper read from the
|
||||
# remote's capy/support.env, `unknown` when its fetch failed, or ""), LIVE.
|
||||
# GSTACK_CAPY_WRAPPER_PATH (optional) names the wrapper a below-minimum fix runs.
|
||||
|
||||
. "$BIN/gstack-doctor-components.sh" || { echo "gstack-doctor: $BIN/gstack-doctor-components.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
. "$BIN/gstack-install-registry.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-install-registry.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
@@ -230,19 +233,32 @@ _dc_check_cso() {
|
||||
_dc_fail_or_skip cso "native helper not built${reason:+ ($reason)}" "cd $SRC && ./setup (it names any missing build prerequisite)" COMPONENT_MISSING "optional; toolchain absent or helper not built${reason:+: $reason}"
|
||||
}
|
||||
|
||||
# _dc_launch_note — when a supported Bun sits off PATH (gstack-capy-install
|
||||
# leaves one in $BUN_INSTALL/bin), sets _dc_ln_dir/_dc_ln_ver/_dc_ln_note and
|
||||
# _dc_ln_fix so a fresh shell's FAIL says where it is and how to launch with it.
|
||||
_dc_launch_note() {
|
||||
_dc_ln_dir=$(gstack_bun_launch_dir 2>/dev/null) || _dc_ln_dir=""
|
||||
_dc_ln_ver="" _dc_ln_note="" _dc_ln_fix=""
|
||||
[ -n "$_dc_ln_dir" ] || return 1
|
||||
_dc_ln_ver=$("$_dc_ln_dir/bun" --version </dev/null 2>/dev/null | clamp 'A-Za-z0-9.+-' 40)
|
||||
_dc_ln_note="; verified bun $_dc_ln_ver found at $_dc_ln_dir (not on PATH)"
|
||||
_dc_ln_fix="export PATH=\"$_dc_ln_dir:\$PATH\" or prefix commands with the GSTACK_LAUNCH line"
|
||||
}
|
||||
|
||||
_dc_check_runtime() {
|
||||
local where found fix="install Bun $GSTACK_BUN_TESTED with the checksum-verified recipe in $SRC/bin/gstack-capy-install (or: bun upgrade), then cd $SRC && ./setup"
|
||||
_dc_launch_note && fix="$_dc_ln_fix"
|
||||
if ! where=$(command -v bun 2>/dev/null); then
|
||||
_dc_row FAIL runtime "bun not found on PATH (gstack needs $GSTACK_BUN_TESTED; floor $GSTACK_BUN_FLOOR)" "$fix" RUNTIME_BELOW_MINIMUM
|
||||
_dc_row FAIL runtime "bun not found on PATH (gstack needs $GSTACK_BUN_TESTED; floor $GSTACK_BUN_FLOOR)$_dc_ln_note" "$fix" RUNTIME_BELOW_MINIMUM
|
||||
return
|
||||
fi
|
||||
found=$(bun --version </dev/null 2>/dev/null | clamp 'A-Za-z0-9.+-' 40)
|
||||
_dc_bun="$found"
|
||||
case "$(gstack_bun_status "$found")" in
|
||||
ok) _dc_row PASS runtime "bun $found at $where (floor $GSTACK_BUN_FLOOR, supported minimum $GSTACK_BUN_TESTED)" ;;
|
||||
untested) _dc_row FAIL runtime "bun $found at $where is below the supported minimum $GSTACK_BUN_TESTED (above the security floor $GSTACK_BUN_FLOOR)" "$fix" RUNTIME_BELOW_MINIMUM ;;
|
||||
too-old) _dc_row FAIL runtime "bun $found at $where is below the security floor $GSTACK_BUN_FLOOR (supported minimum $GSTACK_BUN_TESTED)" "$fix" RUNTIME_BELOW_MINIMUM ;;
|
||||
*) _dc_row FAIL runtime "could not read the version of $where" "$fix" RUNTIME_BELOW_MINIMUM ;;
|
||||
untested) _dc_row FAIL runtime "bun $found at $where is below the supported minimum $GSTACK_BUN_TESTED (above the security floor $GSTACK_BUN_FLOOR)$_dc_ln_note" "$fix" RUNTIME_BELOW_MINIMUM ;;
|
||||
too-old) _dc_row FAIL runtime "bun $found at $where is below the security floor $GSTACK_BUN_FLOOR (supported minimum $GSTACK_BUN_TESTED)$_dc_ln_note" "$fix" RUNTIME_BELOW_MINIMUM ;;
|
||||
*) _dc_row FAIL runtime "could not read the version of $where$_dc_ln_note" "$fix" RUNTIME_BELOW_MINIMUM ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -275,7 +291,17 @@ _dc_check_pins() {
|
||||
pass) _dc_row PASS pins "$project: $summary" ;;
|
||||
none) _dc_row PASS pins "$project pins no runtime (no engines, .tool-versions, .nvmrc, .bun-version or CI pin)" ;;
|
||||
fail) failline=$(printf '%s\n' "$out" | sed -n 's/^PIN_FAIL: //p' | tr '\n' '|' | sed 's/|$//; s/|/; /g')
|
||||
_dc_row FAIL pins "$failline" "install the pinned version for this platform (bin/gstack-capy-install upgrades Bun when gstack and the project agree), or change the pin in $project" PROJECT_PIN_MISMATCH ;;
|
||||
if [ -n "$CHECK_FOR" ] && gstack_doctor_skill_pins_advisory "$CHECK_FOR"; then
|
||||
_dc_row SKIP pins "planning-only; repo pins differ: $failline, launch bun ${_dc_bun:-unknown}"
|
||||
_dc_pins_advisory=1
|
||||
return
|
||||
fi
|
||||
local fix="install the pinned version for this platform (bin/gstack-capy-install installs gstack's verified Bun alongside when a pin names it), or change the pin in $project" note=""
|
||||
if [ -n "$_dc_ln_dir" ] && [ "$_dc_ln_ver" != "$_dc_bun" ] \
|
||||
&& bun "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
|
||||
note="; bun $_dc_ln_ver found at $_dc_ln_dir (not on PATH) satisfies them" fix="$_dc_ln_fix"
|
||||
fi
|
||||
_dc_row FAIL pins "$failline$note" "$fix" PROJECT_PIN_MISMATCH ;;
|
||||
*) _dc_row SKIP pins "gstack-runtime-pins could not evaluate $project (exit $rc: $(printf '%s\n' "$out" | grep -v '^[[:space:]]*$' | head -1 | clamp ' -~' 120))" ;;
|
||||
esac
|
||||
}
|
||||
@@ -317,25 +343,61 @@ _dc_check_cores() {
|
||||
fi
|
||||
}
|
||||
|
||||
# _dc_version_below A B — true when dotted numeric version A sorts below B.
|
||||
_dc_version_below() {
|
||||
local a="$1." b="$2." x y
|
||||
while [ -n "$a" ] || [ -n "$b" ]; do
|
||||
x="${a%%.*}" y="${b%%.*}"; a="${a#*.}" b="${b#*.}"
|
||||
case "$x" in ''|*[!0-9]*) x=0 ;; esac
|
||||
case "$y" in ''|*[!0-9]*) y=0 ;; esac
|
||||
[ "$((10#$x))" -lt "$((10#$y))" ] && return 0
|
||||
[ "$((10#$x))" -gt "$((10#$y))" ] && return 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# _dc_check_min_revision INSTALLED HEAD — sets the PASS-row suffix _dc_min_note,
|
||||
# or prints the REVISION_BELOW_MINIMUM FAIL row for --min-revision and returns 1.
|
||||
# Returns 1 after printing a FAIL row.
|
||||
_dc_check_min_revision() {
|
||||
local installed="$1" head="$2" fix
|
||||
_dc_min_note=""
|
||||
case "$CHECK_MIN_REVISION" in
|
||||
'') return 0 ;;
|
||||
unknown*) _dc_min_note=" (remote minimum unknown (fetch failed); not compared)"; return 0 ;;
|
||||
esac
|
||||
if _dc_version_below "$installed" "$CHECK_MIN_REVISION"; then
|
||||
if [ -n "${GSTACK_CAPY_WRAPPER_PATH:-}" ]; then
|
||||
fix="bash $GSTACK_CAPY_WRAPPER_PATH${CHECK_FOR:+ --for $CHECK_FOR} (fast-forwards), or $SRC/bin/gstack-upgrade"
|
||||
else
|
||||
fix="$SRC/bin/gstack-upgrade (fast-forwards to the remote default branch, then reinstalls)"
|
||||
fi
|
||||
_dc_row FAIL revision "installed $installed${head:+ at $head} is below the supported minimum $CHECK_MIN_REVISION" "$fix" REVISION_BELOW_MINIMUM
|
||||
return 1
|
||||
fi
|
||||
_dc_min_note=" (>= supported minimum $CHECK_MIN_REVISION)"
|
||||
}
|
||||
|
||||
_dc_check_revision() {
|
||||
local installed head="" want="$CHECK_REVISION" target=""
|
||||
installed=$(head -1 "$ROOT/VERSION" 2>/dev/null | clamp '0-9A-Za-z.+-' 40)
|
||||
head=$(git -C "$ROOT_REAL" rev-parse --short HEAD 2>/dev/null | clamp '0-9a-f' 12)
|
||||
_dc_revision="${installed:-unknown}"
|
||||
_dc_check_min_revision "${installed:-0}" "$head" || return
|
||||
if [ -z "$want" ]; then
|
||||
_dc_row PASS revision "${installed:-unknown}${head:+ ($head)}"
|
||||
_dc_row PASS revision "${installed:-unknown}${head:+ ($head)}$_dc_min_note"
|
||||
return
|
||||
fi
|
||||
if [ "$want" = "$installed" ] || [ "$want" = "v$installed" ]; then
|
||||
_dc_row PASS revision "$installed${head:+ ($head)} matches --revision $want"
|
||||
_dc_row PASS revision "$installed${head:+ ($head)} matches --revision $want$_dc_min_note"
|
||||
return
|
||||
fi
|
||||
target=$(git -C "$ROOT_REAL" rev-parse --verify --quiet "$want^{commit}" 2>/dev/null | clamp '0-9a-f' 40)
|
||||
if [ -n "$target" ] && [ "$target" = "$(git -C "$ROOT_REAL" rev-parse HEAD 2>/dev/null)" ]; then
|
||||
_dc_row PASS revision "$installed ($head) is --revision $want"
|
||||
_dc_row PASS revision "$installed ($head) is --revision $want$_dc_min_note"
|
||||
return
|
||||
fi
|
||||
_dc_row FAIL revision "installed ${installed:-unknown}${head:+ at $head} (requested $want)" "cd $SRC && git fetch origin && git checkout $want && ./setup, or run /gstack-upgrade" REVISION_UNMET
|
||||
_dc_row FAIL revision "installed ${installed:-unknown}${head:+ at $head} (requested $want)" "cd $SRC && git fetch origin && git checkout $want && ./setup, or run $SRC/bin/gstack-upgrade to move to the latest release (interactive: /gstack-upgrade)" REVISION_UNMET
|
||||
}
|
||||
|
||||
_dc_print_json() {
|
||||
@@ -373,7 +435,7 @@ ROWS
|
||||
# gstack_doctor_check — the entry point; returns 0 (ok) or 1 (fail).
|
||||
gstack_doctor_check() {
|
||||
local id
|
||||
_dc_bun="" _dc_revision=""
|
||||
_dc_bun="" _dc_revision="" _dc_pins_advisory=0 _dc_ln_dir="" _dc_ln_ver="" _dc_ln_fix=""
|
||||
_dc_resolve_required
|
||||
[ "$CHECK_JSON" -eq 1 ] || echo "required: $_dc_required"
|
||||
for id in $GSTACK_DOCTOR_COMPONENTS; do
|
||||
|
||||
@@ -75,6 +75,14 @@ gstack_doctor_skill_optional() {
|
||||
esac
|
||||
}
|
||||
|
||||
# gstack_doctor_skill_pins_advisory SKILL — true for a planning-only workflow (a repo test-pin mismatch SKIPs pins).
|
||||
gstack_doctor_skill_pins_advisory() {
|
||||
case "$1" in
|
||||
autoplan|plan-ceo-review|plan-eng-review|plan-design-review|plan-devex-review|office-hours) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# gstack_result_anchor CODE — the docs/troubleshooting.md anchor for a lib/result-codes.ts code; returns 1 for an unknown code.
|
||||
gstack_result_anchor() {
|
||||
case "$1" in
|
||||
@@ -166,6 +174,11 @@ gstack_result_anchor() {
|
||||
CONTRIBUTOR_SOURCE_UNAVAILABLE) printf '%s' 'contributor-source-unavailable' ;;
|
||||
BRIEF_INPUT_MISSING) printf '%s' 'brief-input-missing' ;;
|
||||
DRAFT_DIRECTION_UNRESOLVED) printf '%s' 'draft-direction-unresolved' ;;
|
||||
UPGRADE_REFUSED) printf '%s' 'upgrade-refused' ;;
|
||||
UPGRADE_FETCH_FAILED) printf '%s' 'upgrade-fetch-failed' ;;
|
||||
REVISION_BELOW_MINIMUM) printf '%s' 'revision-below-minimum' ;;
|
||||
CAPY_WRAPPER_STALE) printf '%s' 'capy-wrapper-stale' ;;
|
||||
INSTALL_UNVERIFIED) printf '%s' 'install-unverified' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
+77
-2
@@ -1126,10 +1126,11 @@ constraint with its source), or change the project's pin.
|
||||
revision (a VERSION string, tag, branch or commit) and the checkout is at
|
||||
another. An unmet revision is never `ok`.
|
||||
|
||||
**Fix.** Move the checkout: `/gstack-upgrade`, or
|
||||
**Fix.** Move the checkout: `<checkout>/bin/gstack-upgrade` (the tip of the default
|
||||
branch; `/gstack-upgrade` interactively), or
|
||||
`git -C <checkout> fetch origin && git -C <checkout> checkout <ref> && ./setup`.
|
||||
`bin/gstack-capy-install --revision <ref>` never moves an existing checkout; it
|
||||
reports `gstack: ok <installed> (requested <ref>; run gstack-upgrade to move)`.
|
||||
reports `gstack: ok <installed> (requested <ref>; run <checkout>/bin/gstack-upgrade to move)`.
|
||||
|
||||
<a id="capy-install-runtime-conflict"></a>
|
||||
### `gstack-capy-install: Bun conflict: gstack needs >=<minimum>, <project> pins <constraint> (RUNTIME_PIN_CONFLICT; ...)`
|
||||
@@ -1170,6 +1171,80 @@ or nvm), then re-run `bin/gstack-capy-install`. A checksum mismatch means the
|
||||
upstream installer changed: update `GSTACK_BUN_INSTALL_SHA` only after
|
||||
reviewing the new script.
|
||||
|
||||
<a id="upgrade-refused"></a>
|
||||
### `gstack-upgrade: <cause>; nothing was changed; fix: <command> (UPGRADE_REFUSED)`
|
||||
|
||||
**Meaning.** `bin/gstack-upgrade` moves a checkout only by fast-forward to the
|
||||
tip of its remote default branch. It refused before changing anything: the
|
||||
directory is not a gstack checkout, HEAD is detached or on another branch (a
|
||||
pin), a tracked file is modified, a local commit is not on the remote branch,
|
||||
the incoming release's Bun floor is above the Bun on PATH, or another updater
|
||||
holds `<state root>/.setup-lock`. It never stashes, resets or discards.
|
||||
|
||||
**Fix.** Resolve the named cause with the printed command (commit or move the
|
||||
listed changes, check out the default branch, move local commits to another
|
||||
branch), then re-run `bin/gstack-upgrade`. A reinstall that failed after the
|
||||
fast-forward is completed by the next run (`reinstall pending from <version>`).
|
||||
|
||||
<a id="upgrade-fetch-failed"></a>
|
||||
### `gstack-upgrade: could not reach origin (...) (UPGRADE_FETCH_FAILED)`
|
||||
|
||||
**Meaning.** The remote default branch could not be listed or fetched (offline,
|
||||
proxy, credentials), so `--check` cannot say whether the checkout is current.
|
||||
It never reports `current` without the fetch.
|
||||
|
||||
**Fix.** Check network access to the checkout's origin
|
||||
(`git -C <checkout> remote -v`), then re-run.
|
||||
|
||||
<a id="revision-below-minimum"></a>
|
||||
### `gstack: fail revision — installed <v> is below the supported minimum <m> (<cause>); fix: ... (REVISION_BELOW_MINIMUM)`
|
||||
|
||||
**Meaning.** `capy/support.env` on the remote default branch declares the
|
||||
oldest revision whose installer, trailer and fix lines are honest
|
||||
(`GSTACK_MIN_REVISION`), and this checkout is older. The Capy skill
|
||||
bootstrapper reads it from `FETCH_HEAD`, so even a checkout that predates the
|
||||
file learns it. `--check` only reports (`fast-forward available`); install
|
||||
mode fast-forwards a clean, unpinned checkout, reinstalls and rechecks under
|
||||
one lock. The cause names why nothing moved: `pinned by --revision`, `pinned:
|
||||
HEAD is detached`, `tracked files changed: <files>`, `diverged: ...` or
|
||||
`auto-upgrade disabled` (`GSTACK_CAPY_AUTO_UPGRADE=0`). `gstack-doctor --check
|
||||
--min-revision <m>` fails the revision row the same way.
|
||||
|
||||
**Fix.** Run the fix clause: the bootstrapper without `--check`, or
|
||||
`<checkout>/bin/gstack-upgrade`. For a pin, drop `--revision` or pin a
|
||||
revision at or above the minimum. An interrupted install after the
|
||||
fast-forward leaves the trailer `fail`; the next install run completes it.
|
||||
|
||||
<a id="capy-wrapper-stale"></a>
|
||||
### `gstack-capy-skill: owner action — Drive copy is wrapper <n>, minimum <m>; copy <root>/capy/skill/gstack/ (CAPY_WRAPPER_STALE)`
|
||||
|
||||
**Meaning.** The Capy skill folder that ran (the bootstrapper's
|
||||
`GSTACK_CAPY_WRAPPER`) is older than the checkout's `GSTACK_CAPY_MIN_WRAPPER`.
|
||||
The run continues through the checkout's own wrapper (`bin/gstack-capy-session`);
|
||||
only a checkout with no such wrapper refuses (`gstack: fail wrapper`).
|
||||
|
||||
**Fix.** The owner copies `<checkout>/capy/skill/gstack/` over the skill folder
|
||||
(on Capy, the user-scope Drive `skills/gstack/`). Agents never write it.
|
||||
|
||||
<a id="install-unverified"></a>
|
||||
### `INSTALL: fail verdict — <detail>; fix: ... (INSTALL_UNVERIFIED)`
|
||||
|
||||
**Meaning.** On a Capy machine (`GSTACK_CAPY=1`, set by the `GSTACK_LAUNCH`
|
||||
line), an unattended or spawned workflow starts only with a passing install
|
||||
verdict (`<state root>/install-verdicts/<sha256 of the checkout path>.json`,
|
||||
named as `GSTACK_INSTALL_VERDICT`) for this checkout, its current HEAD and a
|
||||
workflow at least as wide as the one starting: a verdict for `--for autoplan`
|
||||
does not cover `qa`'s browser, and a planning-only pins SKIP does not cover a
|
||||
workflow that runs the repo's tests. `gstack-skill-start` printed this row and
|
||||
`GSTACK_RESULT: skill=<s> status=refused run=-`, exit 3. The same start refuses
|
||||
an unattended run whose Bun on PATH is below the supported minimum
|
||||
(`INSTALL: fail runtime ... (RUNTIME_BELOW_MINIMUM)`) on any host.
|
||||
|
||||
**Fix.** Run the Capy skill installer with `--for <workflow>` on this machine
|
||||
and launch with the `GSTACK_LAUNCH` line it prints.
|
||||
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (printed as
|
||||
`INSTALL_VERDICT: overridden`); record it as a deviation.
|
||||
|
||||
---
|
||||
|
||||
## Browser
|
||||
|
||||
@@ -132,6 +132,17 @@ export const SKILL_REQUIREMENTS: Readonly<Record<string, SkillRequirement>> = {
|
||||
unfreeze: CLAUDE_ONLY,
|
||||
};
|
||||
|
||||
/**
|
||||
* Planning-only workflows: they read and review a plan but never run the
|
||||
* target repository's tests, so a repo test-pin mismatch is reported as
|
||||
* `SKIP pins (planning-only; ...)` for `--for <skill>` instead of failing.
|
||||
* Workflows that run the repo's tests (ship, review, qa, pregate) keep `pins`
|
||||
* required.
|
||||
*/
|
||||
export const PINS_ADVISORY_SKILLS: readonly string[] = [
|
||||
'autoplan', 'plan-ceo-review', 'plan-eng-review', 'plan-design-review', 'plan-devex-review', 'office-hours',
|
||||
];
|
||||
|
||||
/** Rows always evaluated and required, in print order. */
|
||||
export const CORE_IDS: readonly ComponentId[] = DOCTOR_COMPONENTS.filter(c => c.kind === 'core').map(c => c.id);
|
||||
export const OPTIONAL_IDS: readonly ComponentId[] = DOCTOR_COMPONENTS.filter(c => c.kind === 'optional').map(c => c.id);
|
||||
@@ -201,6 +212,14 @@ export function renderDoctorComponentsSh(): string {
|
||||
' esac',
|
||||
'}',
|
||||
'',
|
||||
'# gstack_doctor_skill_pins_advisory SKILL — true for a planning-only workflow (a repo test-pin mismatch SKIPs pins).',
|
||||
'gstack_doctor_skill_pins_advisory() {',
|
||||
' case "$1" in',
|
||||
caseArm(PINS_ADVISORY_SKILLS, 'return 0'),
|
||||
' *) return 1 ;;',
|
||||
' esac',
|
||||
'}',
|
||||
'',
|
||||
'# gstack_result_anchor CODE — the docs/troubleshooting.md anchor for a lib/result-codes.ts code; returns 1 for an unknown code.',
|
||||
'gstack_result_anchor() {',
|
||||
' case "$1" in',
|
||||
|
||||
@@ -466,6 +466,32 @@ export const RESULT_CODES = {
|
||||
summary: 'a `draft direction stands until the owner decides` line names a decision that is still pending',
|
||||
fix: 'answer the gate (`gstack-autoplan answer`) before closing; the close rewrites only decided lines',
|
||||
},
|
||||
// Field wave lane A (items 1-4): gstack-upgrade, the Capy skill bootstrapper, install admission.
|
||||
UPGRADE_REFUSED: {
|
||||
anchor: 'upgrade-refused',
|
||||
summary: 'bin/gstack-upgrade changed nothing: the checkout is dirty, diverged, pinned (detached or off the remote default branch), not a gstack checkout, or the incoming release holds',
|
||||
fix: 'resolve the named cause (commit or move the listed changes, check out the default branch), then re-run bin/gstack-upgrade; it never stashes or resets',
|
||||
},
|
||||
UPGRADE_FETCH_FAILED: {
|
||||
anchor: 'upgrade-fetch-failed',
|
||||
summary: 'bin/gstack-upgrade could not fetch the remote default branch, so it cannot tell whether the checkout is current',
|
||||
fix: 'check network access to the checkout\'s origin (git -C <checkout> remote -v), then re-run bin/gstack-upgrade',
|
||||
},
|
||||
REVISION_BELOW_MINIMUM: {
|
||||
anchor: 'revision-below-minimum',
|
||||
summary: 'the installed gstack revision is below the supported minimum declared in capy/support.env on the remote default branch',
|
||||
fix: 'run the Capy skill installer without --check (it fast-forwards a clean, unpinned checkout), or bin/gstack-upgrade; a pinned, dirty or diverged checkout is named in the row',
|
||||
},
|
||||
CAPY_WRAPPER_STALE: {
|
||||
anchor: 'capy-wrapper-stale',
|
||||
summary: 'the Capy skill copy that ran is older than the checkout\'s GSTACK_CAPY_MIN_WRAPPER',
|
||||
fix: 'ask the owner to copy <checkout>/capy/skill/gstack/ over the Drive skill folder; the checkout wrapper keeps working meanwhile',
|
||||
},
|
||||
INSTALL_UNVERIFIED: {
|
||||
anchor: 'install-unverified',
|
||||
summary: 'a Capy session (GSTACK_CAPY=1) has no passing install verdict for this checkout, HEAD and workflow, so the workflow refused to start',
|
||||
fix: 'run the Capy skill installer with --for <workflow> on this machine and launch with the GSTACK_LAUNCH line it prints',
|
||||
},
|
||||
} as const satisfies Record<string, ResultCode>;
|
||||
|
||||
export type ResultCodeName = keyof typeof RESULT_CODES;
|
||||
|
||||
@@ -701,3 +701,69 @@ describe('gstack-doctor --check', () => {
|
||||
expect(doctor(f, ['--help']).out).toContain('--check');
|
||||
});
|
||||
});
|
||||
|
||||
// Field wave lane A: the trailer agrees with the Bun workflows run (A3), a
|
||||
// remote minimum fails the revision row (A2), and planning-only workflows do
|
||||
// not fail on a repo test-pin mismatch (A3 amendment).
|
||||
describe('gstack-doctor --check: launch Bun, supported minimum, planning-only pins', () => {
|
||||
const VERSION = fs.readFileSync(path.join(REPO, 'VERSION'), 'utf8').trim();
|
||||
const check = (f: Fixture, args: string[], extra: Record<string, string> = {}) => {
|
||||
const r = doctor(f, ['--check', ...args], extra);
|
||||
const lines = r.out.split('\n');
|
||||
const rows = Object.fromEntries(lines.filter(l => /^(PASS|FAIL|SKIP) /.test(l)).map(l => {
|
||||
const m = l.match(/^(PASS|FAIL|SKIP) ([a-z-]+)(?:\s(.*))?$/)!;
|
||||
return [m[2], { status: m[1], rest: m[3] ?? '' }];
|
||||
}));
|
||||
return { status: r.status, out: r.out, rows, trailer: lines.filter(l => l.startsWith('gstack: ')).at(-1) ?? '' };
|
||||
};
|
||||
/** A verified Bun in $HOME/.bun/bin (off PATH) that answers --version and runs TypeScript. */
|
||||
const verifiedBun = (f: Fixture, version = '1.4.2') =>
|
||||
script(path.join(f.home, '.bun/bin/bun'), [`[ "$1" = --version ] && { echo "${version}"; exit 0; }`, `exec "${process.execPath}" "$@"`]);
|
||||
|
||||
test('PATH Bun 1.3.14 with a verified 1.4.2 off PATH: runtime FAILs naming where it was found; with the launch PATH it PASSes', () => {
|
||||
const f = makeFixture();
|
||||
verifiedBun(f);
|
||||
const fresh = check(f, [], { STUB_BUN_VERSION: '1.3.14' });
|
||||
expect(fresh.rows.runtime.status).toBe('FAIL');
|
||||
expect(fresh.rows.runtime.rest).toContain(`bun 1.4.2 found at ${f.home}/.bun/bin (not on PATH)`);
|
||||
expect(fresh.rows.runtime.rest).toContain(`fix: export PATH="${f.home}/.bun/bin:$PATH" or prefix commands with the GSTACK_LAUNCH line`);
|
||||
expect(fresh.rows.runtime.rest).toContain('(RUNTIME_BELOW_MINIMUM; ');
|
||||
const launched = check(f, [], { STUB_BUN_VERSION: '1.3.14', PATH: `${f.home}/.bun/bin${path.delimiter}${f.env.PATH}` });
|
||||
expect(launched.rows.runtime.status, launched.out).toBe('PASS');
|
||||
expect(launched.rows.runtime.rest).toContain(`at ${f.home}/.bun/bin/bun`);
|
||||
});
|
||||
|
||||
test('--min-revision: below FAILs with REVISION_BELOW_MINIMUM naming the upgrade command; at or above PASSes; unknown says so and never claims the minimum', () => {
|
||||
const f = makeFixture();
|
||||
const below = check(f, ['--min-revision', '99.0.0.0']);
|
||||
expect(below.status).toBe(1);
|
||||
expect(below.rows.revision.status).toBe('FAIL');
|
||||
expect(below.rows.revision.rest).toContain(`installed ${VERSION}`);
|
||||
expect(below.rows.revision.rest).toContain('below the supported minimum 99.0.0.0');
|
||||
expect(below.rows.revision.rest).toContain(`${bashPath(f.root)}/bin/gstack-upgrade`);
|
||||
expect(below.rows.revision.rest).toContain('(REVISION_BELOW_MINIMUM; ');
|
||||
expect(below.trailer).toBe('gstack: fail revision');
|
||||
const wrapped = check(f, ['--min-revision', '99.0.0.0'], { GSTACK_CAPY_WRAPPER_PATH: '/drive/skills/gstack/scripts/install.sh' });
|
||||
expect(wrapped.rows.revision.rest).toContain('fix: bash /drive/skills/gstack/scripts/install.sh (fast-forwards)');
|
||||
const at = check(f, ['--min-revision', VERSION]);
|
||||
expect(at.rows.revision.status, at.out).toBe('PASS');
|
||||
expect(at.rows.revision.rest).toContain(`>= supported minimum ${VERSION}`);
|
||||
const unknown = check(f, ['--min-revision', 'unknown']);
|
||||
expect(unknown.rows.revision.status).toBe('PASS');
|
||||
expect(unknown.rows.revision.rest).toContain('remote minimum unknown (fetch failed)');
|
||||
expect(unknown.rows.revision.rest).not.toContain('>= supported minimum');
|
||||
});
|
||||
|
||||
test('planning-only workflows SKIP a repo test-pin mismatch; test-running workflows and --require keep pins required', () => {
|
||||
const f = makeFixture();
|
||||
const project = path.join(f.base, 'project');
|
||||
write(path.join(project, '.github/workflows/ci.yml'), 'jobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: oven-sh/setup-bun@v2\n with:\n bun-version: 1.4.2\n');
|
||||
const planning = check(f, ['--for', 'autoplan', '--project', project], { STUB_BUN_VERSION: '1.4.3' });
|
||||
expect(planning.rows.pins.status, planning.out).toBe('SKIP');
|
||||
expect(planning.rows.pins.rest).toMatch(/^\(planning-only; repo pins .*1\.4\.2.*, launch bun 1\.4\.3\)$/);
|
||||
for (const args of [['--for', 'review'], ['--for', 'ship'], ['--require', 'claude']]) {
|
||||
const tests = check(f, [...args, '--project', project], { STUB_BUN_VERSION: '1.4.3' });
|
||||
expect(tests.rows.pins.status, args.join(' ')).toBe('FAIL');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user