fix(wrapper): GSTACK_CAPY_MIN_WRAPPER back to 1; an active wrapper below the minimum is a WARN; gstack-upgrade never leaves a pending marker for the wrapper row and maps every installer stop to a trailer (integration items 2, 9)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-15 01:18:25 +00:00
1 parent 5630ea3c34
commit d0afbe9c4d
5 files changed
+83 -16

No files matched your search

+3 -2
View File
@@ -432,7 +432,8 @@ _dc_wrapper_rev() {
# _dc_check_wrapper — the Capy skill copy. The active wrapper's provenance
# (GSTACK_CAPY_WRAPPER_REV/PATH, exported by the bootstrapper) decides: current
# passes, and a stale Drive copy elsewhere is a WARN for the owner. Without
# passes; an active copy below the minimum, or a stale Drive copy elsewhere, is
# a WARN for the owner (an older copy still works, capy/support.env). Without
# provenance (a direct doctor run, or a wrapper too old to export it) the Drive
# copies decide: GSTACK_CAPY_WRAPPER_PATH when it names one, else every
# ~/.capy/drive/*/skills/gstack/scripts/install.sh, failing only when each is
@@ -455,7 +456,7 @@ _dc_check_wrapper() {
fi
if [ -n "${GSTACK_CAPY_WRAPPER_REV:-}" ]; then
if [ "$GSTACK_CAPY_WRAPPER_REV" -lt "$min" ] 2>/dev/null; then
_dc_row FAIL wrapper "the active Capy skill copy ${active:-<unknown>} is wrapper $GSTACK_CAPY_WRAPPER_REV, minimum $min" "$workaround; owner: $owner" CAPY_WRAPPER_STALE
_dc_row WARN wrapper "the active Capy skill copy ${active:-<unknown>} is wrapper $GSTACK_CAPY_WRAPPER_REV, minimum $min (it still works)" "$owner" CAPY_WRAPPER_STALE
elif [ -n "$stale" ]; then
_dc_row WARN wrapper "active wrapper $GSTACK_CAPY_WRAPPER_REV (${active:-unknown path}) is current; stale Drive copies (minimum $min): $stale" "$owner" CAPY_WRAPPER_STALE
else
+39 -9
View File
@@ -77,6 +77,23 @@ REMOTE_VER=$(git -C "$ROOT" show FETCH_HEAD:VERSION 2>/dev/null | head -1)
PENDING="$(git -C "$ROOT" rev-parse --absolute-git-dir)/gstack-upgrade.pending"
CAPY=0
[ -f "$(git -C "$ROOT" rev-parse --absolute-git-dir)/gstack-capy-install.state" ] && [ -x "$ROOT/bin/gstack-capy-install" ] && CAPY=1
# Run outside a Capy session, the installer this script starts is the
# checkout's own; say so, so the doctor's wrapper row judges that copy.
if [ "$CAPY" -eq 1 ] && [ -z "${GSTACK_CAPY_WRAPPER_REV:-}" ] && [ -f "$ROOT/capy/skill/gstack/scripts/install.sh" ]; then
GSTACK_CAPY_WRAPPER_REV=$(sed -n 's/^GSTACK_CAPY_WRAPPER=\([0-9][0-9]*\).*/\1/p' "$ROOT/capy/skill/gstack/scripts/install.sh" | head -1)
[ -n "$GSTACK_CAPY_WRAPPER_REV" ] && export GSTACK_CAPY_WRAPPER_REV GSTACK_CAPY_WRAPPER_PATH="$ROOT/capy/skill/gstack/scripts/install.sh"
fi
# stop_trailer ERRFILE — an installer stop line (exit 3, before ./setup) as a
# trailer: a RUNTIME_PIN_CONFLICT is `gstack: fail pins`, any other stop
# `gstack: fail runtime`.
stop_trailer() {
_stop=$(grep -E '^gstack-capy-install: .*\([A-Z][A-Z_]*;' "$1" | tail -1)
case "$_stop" in
'') ;;
*'(RUNTIME_PIN_CONFLICT;'*) echo "gstack: fail pins — ${_stop#gstack-capy-install: }" ;;
*) echo "gstack: fail runtime — ${_stop#gstack-capy-install: }" ;;
esac
}
# certify — the caller's project (see the header); exits with its outcome.
certify() {
[ -n "$CALLER_PROJECT" ] && [ "$REINSTALL" -eq 1 ] || exit 0
@@ -84,10 +101,10 @@ certify() {
_err=$(mktemp "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX")
(cd "$CALLER" && "$ROOT/bin/gstack-capy-install" --project "$CALLER_PROJECT" </dev/null 2>"$_err")
_rc=$?
_conflict=$(grep '(RUNTIME_PIN_CONFLICT;' "$_err" | tail -1)
cat "$_err" >&2; rm -f "$_err"
[ "$_rc" -eq 0 ] && exit 0
[ -n "$_conflict" ] && echo "gstack: fail pins — ${_conflict#gstack-capy-install: }"
cat "$_err" >&2
[ "$_rc" -eq 0 ] && { rm -f "$_err"; exit 0; }
[ "$_rc" -eq 3 ] && stop_trailer "$_err"
rm -f "$_err"
exit 1
fi
(cd "$CALLER" && "$ROOT/bin/gstack-doctor" --check --require runtime,pins --project "$CALLER_PROJECT" </dev/null) && exit 0
@@ -97,19 +114,32 @@ reinstall() { # FROM — run the install step; a pending marker survives a failu
# With a project to certify next, the installer's own trailer is held back
# (printed only when the reinstall fails), so no ok precedes a pins failure.
printf '%s\n' "$1" > "$PENDING"
_held=$(mktemp "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX")
_held=$(mktemp "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX") _ierr=$(mktemp "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX")
if [ "$CAPY" -eq 1 ]; then
(cd "$ROOT" && GSTACK_CAPY_UPGRADED_FROM="$1" "$ROOT/bin/gstack-capy-install" </dev/null) | while IFS= read -r _line; do
case "$_line" in "gstack: "*) [ -n "$CALLER_PROJECT" ] && { printf '%s\n' "$_line" > "$_held"; continue; } ;; esac
(cd "$ROOT" && GSTACK_CAPY_UPGRADED_FROM="$1" "$ROOT/bin/gstack-capy-install" </dev/null 2>"$_ierr") | while IFS= read -r _line; do
case "$_line" in "gstack: "*) printf '%s\n' "$_line" > "$_held"; continue ;; esac
printf '%s\n' "$_line"
done
_rc=${PIPESTATUS[0]}
cat "$_ierr" >&2
else
(cd "$ROOT" && ./setup --refresh-registered </dev/null)
_rc=$?
fi
if [ "$_rc" -eq 0 ]; then rm -f "$PENDING" "$_held"; certify; fi
cat "$_held"; rm -f "$_held"
# The wrapper row is an owner action on the Drive copy, never a reinstall
# failure: no pending marker for it.
if [ "$_rc" -ne 0 ] && [ "$(cat "$_held")" = "gstack: fail wrapper" ]; then
rm -f "$PENDING"
echo "gstack-upgrade: reinstalled $(head -1 "$ROOT/VERSION"); only the wrapper row failed (an owner action on the Drive copy, CAPY_WRAPPER_STALE), so no reinstall is pending"
fi
if [ "$_rc" -eq 0 ]; then
[ -n "$CALLER_PROJECT" ] || cat "$_held"
rm -f "$PENDING" "$_held" "$_ierr"; certify
fi
[ -s "$_held" ] && cat "$_held"
[ "$_rc" -eq 3 ] && [ ! -s "$_held" ] && stop_trailer "$_ierr"
rm -f "$_held" "$_ierr"
[ -f "$PENDING" ] || exit 1
echo "gstack-upgrade: the checkout is at $(head -1 "$ROOT/VERSION") but its reinstall failed (exit $_rc); fix: re-run $ROOT/bin/gstack-upgrade (it completes the pending reinstall)" >&2
exit 1
}
+1 -1
View File
@@ -16,4 +16,4 @@ GSTACK_MIN_REVISION=1.91.96.0
# GSTACK_CAPY_MIN_WRAPPER: the oldest bootstrapper revision (GSTACK_CAPY_WRAPPER in
# capy/skill/gstack/scripts/install.sh) the owner should run. An older copy still
# works and prints an owner action (CAPY_WRAPPER_STALE).
GSTACK_CAPY_MIN_WRAPPER=2
GSTACK_CAPY_MIN_WRAPPER=1
+6 -3
View File
@@ -829,7 +829,7 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
expect(parse(f, [], { GSTACK_CAPY_WRAPPER_PATH: stale }).trailer).toBe('gstack: fail wrapper');
});
test('the printed workaround certifies: a current active wrapper passes with the stale Drive copy unchanged (WARN, owner action); a stale active wrapper fails', () => {
test('the printed workaround certifies: a current active wrapper passes with the stale Drive copy unchanged (WARN, owner action); a stale active wrapper is a WARN too', () => {
const f = makeFixture();
withSupport(f);
const stale = drive(f, 'user-garry-tan', PRE_BOOTSTRAPPER);
@@ -844,8 +844,11 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
expect(json.rows.find((x: { id: string }) => x.id === 'wrapper')).toMatchObject({ status: 'WARN', code: 'CAPY_WRAPPER_STALE' });
withSupport(f, 2);
const old = parse(f, [], { GSTACK_CAPY_WRAPPER_REV: '1', GSTACK_CAPY_WRAPPER_PATH: stale });
expect(old.trailer).toBe('gstack: fail wrapper');
expect(old.line('wrapper')).toContain(`FAIL wrapper — the active Capy skill copy ${stale} is wrapper 1, minimum 2`);
// Integration item 2: an active copy below the minimum still works, so it
// is an owner-action WARN and the trailer stays ok.
expect(old.status, old.out).toBe(0);
expect(old.trailer).toMatch(/^gstack: ok /);
expect(old.line('wrapper')).toBe(`WARN wrapper — the active Capy skill copy ${stale} is wrapper 1, minimum 2 (it still works); owner action: copy ${f.root}/capy/skill/gstack/ over the Drive skill folder, or have Capy re-sync it; this recurs every session until the Drive copy changes (CAPY_WRAPPER_STALE; https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#capy-wrapper-stale)`);
});
test('a drifted Bun resolution is a runtime FAIL carrying the installer\'s detail, fix and BUN_RESOLUTION_DRIFT', () => {
+34 -1
View File
@@ -238,8 +238,12 @@ describe('gstack-upgrade', () => {
'echo "capy-install $* cwd=$(pwd -P)" >> "$UPGRADE_LOG"',
'case " $* " in *" --project "*)',
' [ -n "${STUB_PIN_CONFLICT:-}" ] && { echo "gstack-capy-install: Bun conflict: gstack needs >=1.4.2 (verified archives: 1.4.2 1.4.3), $2/package.json pins exactly 1.3.14; fix: raise the project\'s pin (RUNTIME_PIN_CONFLICT; https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#capy-install-runtime-conflict)" >&2; exit 3; }',
' [ -n "${STUB_PINS_FAIL:-}" ] && { echo "FAIL pins — bun 1.4.2 outside CI 1.4.3; fix: x (PROJECT_PIN_MISMATCH; d)"; echo "gstack: fail pins"; exit 1; } ;;',
' [ -n "${STUB_PINS_FAIL:-}" ] && { echo "FAIL pins — bun 1.4.2 outside CI 1.4.3; fix: x (PROJECT_PIN_MISMATCH; d)"; echo "gstack: fail pins"; exit 1; }',
' [ -n "${STUB_PROJECT_STOP:-}" ] && { echo "gstack-capy-install: preflight: jq not found; fix: install jq (INSTALL_PREFLIGHT_FAILED; d)" >&2; exit 3; } ;;',
'esac',
'echo "wrapper-env rev=${GSTACK_CAPY_WRAPPER_REV:-} path=${GSTACK_CAPY_WRAPPER_PATH:-}" >> "$UPGRADE_LOG.env"',
'[ -n "${STUB_STOP:-}" ] && { echo "gstack-capy-install: preflight: curl not found; fix: install curl (INSTALL_PREFLIGHT_FAILED; d)" >&2; exit 3; }',
'[ -n "${STUB_WRAPPER_FAIL:-}" ] && { echo "FAIL wrapper — every Capy skill copy is stale; fix: x (CAPY_WRAPPER_STALE; d)"; echo "gstack: fail wrapper"; exit 1; }',
'[ -f "${STUB_FAIL_ONCE:-/nonexistent}" ] && { rm -f "$STUB_FAIL_ONCE"; echo "gstack: fail setup — ./setup failed"; exit 1; }',
'echo "gstack: ok $(cat "$(dirname "$0")/../VERSION") for=autoplan"',
].join('\n') + '\n';
@@ -305,6 +309,35 @@ describe('gstack-upgrade', () => {
expect(f.calls().at(-1)).toBe(`capy-install --project ${f.project} cwd=${f.project}`);
});
// Integration items 2 and 9: the wrapper row is never a reinstall failure,
// and every installer stop line becomes a trailer.
test('a reinstall whose only failing row is wrapper leaves no pending marker; the installer runs with the checkout copy as the active wrapper', () => {
const f = capyFixture();
write(path.join(f.root, 'capy/skill/gstack/scripts/install.sh'), 'GSTACK_CAPY_WRAPPER=7\n');
const r = f.from(fs.realpathSync(f.host.base), [], { STUB_WRAPPER_FAIL: '1' });
expect(r.status).toBe(1);
expect(r.stdout).toContain('gstack: fail wrapper');
expect(r.stdout).toContain('only the wrapper row failed (an owner action on the Drive copy, CAPY_WRAPPER_STALE), so no reinstall is pending');
expect(r.stderr).not.toContain('completes the pending reinstall');
expect(fs.existsSync(f.pending)).toBe(false);
const again = f.from(fs.realpathSync(f.host.base));
expect(again.stdout).not.toContain('reinstall pending');
expect(fs.readFileSync(path.join(f.host.base, 'calls.log.env'), 'utf8')).toContain(`rev=7 path=${f.root}/capy/skill/gstack/scripts/install.sh`);
});
test('an installer stop line maps to `gstack: fail runtime — <msg>`: in the caller-project check (no pending) and in the reinstall (pending kept)', () => {
const f = capyFixture();
const cert = f.from(f.project, [], { STUB_PROJECT_STOP: '1' });
expect(cert.status).toBe(1);
expect(cert.stdout.trim().split('\n').at(-1)).toBe('gstack: fail runtime — preflight: jq not found; fix: install jq (INSTALL_PREFLIGHT_FAILED; d)');
expect(fs.existsSync(f.pending)).toBe(false);
const g = capyFixture();
const re = g.from(g.project, [], { STUB_STOP: '1' });
expect(re.status).toBe(1);
expect(re.stdout.trim().split('\n').at(-1)).toBe('gstack: fail runtime — preflight: curl not found; fix: install curl (INSTALL_PREFLIGHT_FAILED; d)');
expect(fs.existsSync(g.pending)).toBe(true);
});
test('off Capy the caller\'s project is certified by the doctor\'s runtime and pins rows', () => {
const f = fixture();
write(path.join(f.root, 'bin/gstack-doctor'), '#!/usr/bin/env bash\necho "doctor $*" >> "$UPGRADE_LOG"\n[ -n "${STUB_PINS_FAIL:-}" ] && { echo "FAIL pins — x"; echo "gstack: fail pins"; exit 1; }\necho "gstack: ok 1.0.1 for=require=runtime,pins"\n', 0o755);