diff --git a/bin/gstack-capy-install b/bin/gstack-capy-install index 7da110086..13fd12104 100755 --- a/bin/gstack-capy-install +++ b/bin/gstack-capy-install @@ -391,15 +391,30 @@ EOF_SHIM exec 8>&- } -# 4. Certify. The trailer is the contract; `(requested ; ...)` when the -# existing checkout is not at --revision (it is never moved here). +# 4. Certify. The trailer is the contract, in one grammar: +# gstack: ok for=[ (requested: …; pins: skipped (planning-only|no project); latest: available)] +# Notes, in that fixed order, ride only on an ok trailer: `requested` when the +# existing checkout is not at --revision (it is never moved here), `pins` when +# the doctor skipped the project pins, `latest` when the bootstrapper read a +# newer VERSION from origin in this run, exported as GSTACK_CAPY_REMOTE_VERSION. The +# failure head (`gstack: fail `) is unchanged. INSTALLED=$(head -1 "$ROOT/VERSION") -REV_NOTE="" +TRAILER="$(printf '%s\n' "$OUT" | grep '^gstack: ' | tail -1)" +NOTES="" +note() { NOTES="${NOTES:+$NOTES; }$1"; } if [ -n "$REVISION" ] && [ "$REVISION" != "$INSTALLED" ] && [ "$REVISION" != "v$INSTALLED" ] \ && [ "$(git -C "$ROOT" rev-parse --verify --quiet "$REVISION^{commit}" 2>/dev/null)" != "$(git -C "$ROOT" rev-parse HEAD 2>/dev/null)" ]; then - REV_NOTE=" (requested $REVISION; run $ROOT/bin/gstack-upgrade to move)" + note "requested: $REVISION (run $ROOT/bin/gstack-upgrade to move)" fi -TRAILER="$(printf '%s\n' "$OUT" | grep '^gstack: ' | tail -1)$REV_NOTE" +if printf '%s\n' "$OUT" | grep -q '^SKIP pins (planning-only'; then note "pins: skipped (planning-only)" +elif printf '%s\n' "$OUT" | grep -Eq '^SKIP pins \((no project|project directory )'; then note "pins: skipped (no project)" +fi +REMOTE_VERSION="${GSTACK_CAPY_REMOTE_VERSION:-}" +if [ -n "$REMOTE_VERSION" ] && [ "$REMOTE_VERSION" != "$INSTALLED" ] \ + && [ "$(printf '%s\n%s\n' "$INSTALLED" "$REMOTE_VERSION" | sort -V | head -1)" = "$INSTALLED" ]; then + note "latest: $REMOTE_VERSION available" +fi +case "$TRAILER" in "gstack: ok "*) [ -n "$NOTES" ] && TRAILER="$TRAILER ($NOTES)" ;; esac record_verdict "$TRAILER" "$([ "$RC" -eq 0 ] && echo 1 || echo 0)" [ "$CHECK_ONLY" -eq 1 ] || write_shim printf '%s\n' "$OUT" | grep -v '^gstack: ' diff --git a/bin/gstack-capy-session b/bin/gstack-capy-session index eba0e1a52..815919eb7 100755 --- a/bin/gstack-capy-session +++ b/bin/gstack-capy-session @@ -5,6 +5,8 @@ # and execs this script with its own arguments plus: # GSTACK_CAPY_WRAPPER_REV / GSTACK_CAPY_WRAPPER_PATH the bootstrapper that ran # GSTACK_CAPY_REMOTE_MIN the supported minimum from origin's capy/support.env, or `unknown` +# GSTACK_CAPY_REMOTE_VERSION origin's VERSION read in this run (wrapper 2+; the +# installer's `latest: available` trailer note) # GSTACK_CAPY_UPGRADED_FROM the version it fast-forwarded from (setup is never skipped then) # GSTACK_CAPY_LOCK_HELD=1 install mode: .gstack-capy-install.lock is held on an # inherited descriptor; nothing here takes it again diff --git a/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index 3be1861e6..928277f18 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -9,9 +9,9 @@ description: Install and use gstack in Capy and Claude Code. Load at session sta Resolve `scripts/install.sh` relative to this SKILL.md, not the working repository. It is a frozen bootstrapper: it prints `gstack-capy-skill: wrapper ()` first, learns the supported minimum from gstack's remote, and hands over to the checkout's own wrapper (`bin/gstack-capy-session`). -1. At session start run `bash scripts/install.sh --check`, adding `--for ` when you know the workflow (`--for autoplan`, `--for review`, `--for qa`). It prints one `PASS|FAIL|SKIP ` row per component, a `GSTACK_LAUNCH:` line, and exactly one trailer as the last line: `gstack: ok for=` (exit 0) or `gstack: fail …` (exit 1). Read the exit status and grep `^gstack: ` directly; never pipe the check through `tail`, `head` or `grep` alone. `--check` never moves the checkout. +1. At session start run `bash scripts/install.sh --check`, adding `--for ` when you know the workflow (`--for autoplan`, `--for review`, `--for qa`). It prints one `PASS|FAIL|SKIP|WARN ` row per component (`WARN` is an owner action, never a failure), a `GSTACK_LAUNCH:` line, and exactly one trailer as the last line: `gstack: ok for=[ (requested: …; pins: skipped (planning-only|no project); latest: available)]` (exit 0) or `gstack: fail …` (exit 1). Read the exit status and grep `^gstack: ` directly; never pipe the check through `tail`, `head` or `grep` alone. `--check` never moves the checkout. 2. If the trailer names `revision`, `runtime` or `pins` (`gstack: fail revision …`, `gstack: fail runtime,pins`, `gstack: fail pins`) and a checkout exists, or the output carries a `gstack-capy-session: behind origin/: …` line, run the same command without `--check` (900-second timeout). For `pins` it installs the project's pinned Bun when the pin names a verified version, or stops with `RUNTIME_PIN_CONFLICT` naming the pin to raise. Any clean, unpinned checkout behind origin is fast-forwarded in install mode (below the `GSTACK_MIN_REVISION` floor by the bootstrapper, `gstack-capy-skill: upgraded -> `; above it by the checkout, `SELF_UPGRADE: done -> `), reinstalled and rechecked in one locked step. A pinned (`--revision`, detached), dirty or diverged checkout, or one another live session holds a lease on (`UPGRADE_BUSY`), is never moved: below the floor the trailer names the cause and the exact commands; above it the run prints `SELF_UPGRADE: skipped ()` and continues on the installed revision. `GSTACK_CAPY_AUTO_UPGRADE=0` disables every fast-forward, `GSTACK_CAPY_SELF_UPGRADE=off` only the above-floor one. -3. If the trailer is `gstack: fail not-installed …` and this session runs no gstack workflow, record that line and continue; nothing is installed. Install right before the first workflow with `bash scripts/install.sh --for ` (about 30 s cold: clone, the SHA-256-verified Bun 1.4.2, both host profiles without Chromium). +3. If the trailer is `gstack: fail not-installed …` and this session runs no gstack workflow, record that line and continue; nothing is installed. Install right before the first workflow with `bash scripts/install.sh --for ` (about 30 s cold: clone, a SHA-256-verified Bun in gstack's runtime store when no installed Bun satisfies the project's pins, both host profiles without Chromium). It also writes a marked `~/.local/bin/bun` shim, so a fresh shell in a project runs the Bun recorded for it. 4. Any other `FAIL` row carries a `fix:` command and a reason code with a troubleshooting anchor; run the fix, then `--check` again. `SKIP codex-cli (optional; …)`, `SKIP browser (lazy; …)` and `SKIP pins (planning-only; …)` are not failures. **The trailer rule.** No `^gstack: ok` line from this machine in this session means gstack is not installed here: do not run a workflow, and do not claim the skills work. One machine's install never covers another. diff --git a/capy/skill/gstack/scripts/install.sh b/capy/skill/gstack/scripts/install.sh index b420e7c31..6f81370ef 100755 --- a/capy/skill/gstack/scripts/install.sh +++ b/capy/skill/gstack/scripts/install.sh @@ -8,7 +8,9 @@ # 3. in install mode, fast-forward a clean, unpinned checkout below that # minimum, under .gstack-capy-install.lock (--check never moves it); # 4. exec the checkout's full wrapper, bin/gstack-capy-session, with the same -# arguments, handing the held lock over (GSTACK_CAPY_LOCK_HELD=1). +# arguments, handing the held lock over (GSTACK_CAPY_LOCK_HELD=1) and, +# from revision 2, origin's VERSION read in this run +# (GSTACK_CAPY_REMOTE_VERSION, the trailer's `latest:` note). # Everything else (Bun, setup, the doctor, the verdict, the launch line) is # the checkout's. The last line is the `gstack:` trailer; exit 0 only on # `gstack: ok`. Contract: docs/capy-install.md; tests: @@ -20,14 +22,14 @@ # Environment: GSTACK_CAPY_ROOT (default ~/.local/share/gstack), GSTACK_CAPY_REPO, # GSTACK_CAPY_AUTO_UPGRADE=0 (never fast-forward), GSTACK_CAPY_FETCH_TIMEOUT (s). set -u -GSTACK_CAPY_WRAPPER=1 +GSTACK_CAPY_WRAPPER=2 self="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")" root="${GSTACK_CAPY_ROOT:-$HOME/.local/share/gstack}" repo="${GSTACK_CAPY_REPO:-https://github.com/garrytan/gstack.git}" echo "gstack-capy-skill: wrapper $GSTACK_CAPY_WRAPPER ($self)" -check=0 skill="" revision="" min_note="" remote_min="" +check=0 skill="" revision="" min_note="" remote_min="" remote_version="" args=() while [ $# -gt 0 ]; do case "$1" in @@ -80,6 +82,7 @@ if [ ! -f "$root/setup" ]; then || { echo "gstack: fail installer — could not clone $repo; fix: check network access, then $fix_cmd"; exit 1; } fi remote_min=$(support_get "$(cat "$root/capy/support.env" 2>/dev/null)" GSTACK_MIN_REVISION) + remote_version=$(head -1 "$root/VERSION" 2>/dev/null) else # The remote's rules: fetch the default branch into FETCH_HEAD (never --depth # into an existing checkout), read capy/support.env from it. @@ -88,6 +91,7 @@ else if [ -n "$default" ] && bounded git -C "$root" fetch --quiet origin "$default" 2>/dev/null; then support=$(git -C "$root" show FETCH_HEAD:capy/support.env 2>/dev/null) remote_min=$(support_get "$support" GSTACK_MIN_REVISION) + remote_version=$(git -C "$root" show FETCH_HEAD:VERSION 2>/dev/null | head -1) else min_note="remote minimum unknown (fetch failed)" echo "gstack-capy-skill: $min_note; checking the local install only" @@ -127,6 +131,7 @@ if [ ! -x "$session" ]; then exit 1 fi export GSTACK_CAPY_WRAPPER_REV="$GSTACK_CAPY_WRAPPER" GSTACK_CAPY_WRAPPER_PATH="$self" GSTACK_CAPY_REMOTE_MIN="${remote_min:-unknown}" +if [ -n "$remote_version" ]; then export GSTACK_CAPY_REMOTE_VERSION="$remote_version"; else unset GSTACK_CAPY_REMOTE_VERSION; fi [ "$check" -eq 0 ] && export GSTACK_CAPY_LOCK_HELD=1 if [ "$check" -eq 1 ]; then exec "$session" --check --root "$root" ${args[@]+"${args[@]}"} diff --git a/test/capy-skill-wrapper.test.ts b/test/capy-skill-wrapper.test.ts index f51dd7dae..b7f8b6c10 100644 --- a/test/capy-skill-wrapper.test.ts +++ b/test/capy-skill-wrapper.test.ts @@ -138,13 +138,13 @@ describe('capy skill bootstrapper (capy/skill/gstack/scripts/install.sh)', () => expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b); expect(f.git(['rev-parse', '--is-shallow-repository'])).toBe('true'); expect(install.last).toBe('gstack: ok 1.0.1 for=autoplan'); - expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=1 min=1.0.1 from=1.0.0`]); + expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=1.0.0`]); const next = f.run(['--check', '--for', 'autoplan']); expect(next.status, next.stdout + next.stderr).toBe(0); expect(next.stdout).not.toContain('upgraded'); expect(next.last).toBe('gstack: ok 1.0.1 for=autoplan'); - expect(f.calls().at(-1)).toBe(`installer --check --root ${f.root} --for autoplan lock= rev=1 min=1.0.1 from=`); + expect(f.calls().at(-1)).toBe(`installer --check --root ${f.root} --for autoplan lock= rev=2 min=1.0.1 from=`); expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b); }); @@ -242,7 +242,7 @@ describe('capy skill: always current when online (the minimum is the floor, not expect(r.stdout).toMatch(/^SELF_UPGRADE: done 1\.0\.1 -> 1\.0\.2$/m); expect(f.git(['rev-parse', 'HEAD'])).toBe(f.c); expect(r.last).toBe('gstack: ok 1.0.2 for=autoplan'); - expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=1 min=1.0.1 from=1.0.1`]); + expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=1.0.1`]); expect(fs.existsSync(path.join(f.root, '.git/gstack-upgrade.pending'))).toBe(false); const next = f.run(['--for', 'autoplan']); @@ -250,7 +250,7 @@ describe('capy skill: always current when online (the minimum is the floor, not expect(next.stdout).toMatch(/^SELF_UPGRADE: none \(current\)$/m); expect(next.stdout).not.toContain('SELF_UPGRADE: done'); expect(next.last).toBe('gstack: ok 1.0.2 for=autoplan'); - expect(f.calls().at(-1)).toBe(`installer --root ${f.root} --for autoplan lock=1 rev=1 min=1.0.1 from=`); + expect(f.calls().at(-1)).toBe(`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=`); }); test('--check mode never moves a behind checkout: it names the gap and the fast-forwarding command, then certifies the installed revision', () => { @@ -261,7 +261,7 @@ describe('capy skill: always current when online (the minimum is the floor, not expect(r.stdout).not.toContain('SELF_UPGRADE'); expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b); expect(r.last).toBe('gstack: ok 1.0.1 for=autoplan'); - expect(f.calls()).toEqual([`installer --check --root ${f.root} --for autoplan lock= rev=1 min=1.0.1 from=`]); + expect(f.calls()).toEqual([`installer --check --root ${f.root} --for autoplan lock= rev=2 min=1.0.1 from=`]); }); test('a dirty behind checkout is skipped with gstack-upgrade\'s reason and still ends ok: above the floor, the trailer is the doctor\'s', () => { @@ -274,7 +274,7 @@ describe('capy skill: always current when online (the minimum is the floor, not expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b); expect(fs.readFileSync(path.join(f.root, 'README.md'), 'utf8')).toBe('local edit\n'); expect(r.last).toBe('gstack: ok 1.0.1 for=autoplan'); - expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=1 min=1.0.1 from=`]); + expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=`]); }); test('GSTACK_CAPY_SELF_UPGRADE=off and GSTACK_CAPY_AUTO_UPGRADE=0 each disable the fast-forward by name and leave HEAD where it is', () => { @@ -291,6 +291,23 @@ describe('capy skill: always current when online (the minimum is the floor, not }); }); +// Plan A9: the `latest:` trailer note comes only from origin's VERSION the +// bootstrapper read in this run; a FETCH_HEAD left by an earlier run never +// reaches the installer. +describe('capy skill bootstrapper: GSTACK_CAPY_REMOTE_VERSION', () => { + test('a fresh fetch exports origin\'s VERSION; a failed fetch exports nothing even with a stale FETCH_HEAD', () => { + const f = fixture({ cloneAt: 'B' }); + write(path.join(f.root, 'bin/gstack-capy-install'), '#!/usr/bin/env bash\necho "gstack: ok $(cat "$(dirname "$0")/../VERSION") for=autoplan remote=${GSTACK_CAPY_REMOTE_VERSION:-none}"\n', 0o755); + const fresh = f.run(['--check', '--for', 'autoplan'], { GSTACK_CAPY_REMOTE_VERSION: 'from-parent' }); + expect(fresh.last).toBe('gstack: ok 1.0.1 for=autoplan remote=1.0.2'); + expect(f.git(['show', 'FETCH_HEAD:VERSION'])).toBe('1.0.2'); + f.git(['remote', 'set-url', 'origin', `file://${f.host.base}/missing.git`]); + const stale = f.run(['--check', '--for', 'autoplan'], { GSTACK_CAPY_REMOTE_VERSION: 'from-parent' }); + expect(stale.stdout).toContain('remote minimum unknown (fetch failed)'); + expect(stale.last).toBe('gstack: ok 1.0.1 for=autoplan remote=none'); + }); +}); + describe('capy skill: vendored text and support file', () => { const support = fs.existsSync(path.join(REPO, 'capy/support.env')) ? fs.readFileSync(path.join(REPO, 'capy/support.env'), 'utf8') : ''; test('the wrapper revision meets the checkout minimum; GSTACK_MIN_REVISION <= VERSION', () => { diff --git a/test/gstack-capy-install.test.ts b/test/gstack-capy-install.test.ts index d2d59522e..075f086a2 100644 --- a/test/gstack-capy-install.test.ts +++ b/test/gstack-capy-install.test.ts @@ -57,7 +57,7 @@ function fixture(opts: { bun?: string; jq?: boolean } = {}) { script(path.join(root, 'setup'), ['echo "setup $*" >> "$CAPY_LOG"', 'echo "env GSTACK_SKIP_PLAYWRIGHT=${GSTACK_SKIP_PLAYWRIGHT:-} GSTACK_SKIP_FONTS=${GSTACK_SKIP_FONTS:-} bun=$(bun --version)" >> "$CAPY_LOG"', 'exit "${STUB_SETUP_EXIT:-0}"']); script(path.join(root, 'bin/gstack-doctor'), [ 'echo "doctor $* GSTACK_EPHEMERAL=${GSTACK_EPHEMERAL:-}" >> "$CAPY_LOG"', 'echo "doctor-cwd $(pwd -P)" >> "$CAPY_LOG"', - 'echo "required: claude codex patch"', 'echo "PASS claude ok"', 'echo "SKIP browser (lazy)"', + 'echo "required: claude codex patch"', 'echo "PASS claude ok"', 'echo "SKIP browser (lazy)"', '[ -n "${STUB_PINS_SKIP:-}" ] && echo "SKIP pins ($STUB_PINS_SKIP)"', '[ "${STUB_DOCTOR_FAIL:-}" = 1 ] && { echo "FAIL codex — missing; fix: x (COMPONENT_MISSING; d)"; echo "gstack: fail codex"; exit 1; }', '[ -n "${GSTACK_DOCTOR_RUNTIME_DRIFT:-}" ] && { echo "FAIL runtime — bun resolution drifted: $GSTACK_DOCTOR_RUNTIME_DRIFT; fix: $GSTACK_DOCTOR_RUNTIME_DRIFT_FIX (BUN_RESOLUTION_DRIFT; d)"; echo "gstack: fail runtime"; exit 1; }', 'echo "gstack: ok $(cat "$(dirname "$0")/../VERSION") for=autoplan"', @@ -237,12 +237,27 @@ describe('gstack-capy-install', () => { const before = sh('git rev-parse HEAD', f.root).stdout.trim(); const r = run(f, ['--revision', 'v9.9.9.9']); expect(r.status, r.stdout + r.stderr).toBe(0); - expect(lastLine(r.stdout)).toBe(`gstack: ok 1.2.3 for=autoplan (requested v9.9.9.9; run ${f.root}/bin/gstack-upgrade to move)`); + expect(lastLine(r.stdout)).toBe(`gstack: ok 1.2.3 for=autoplan (requested: v9.9.9.9 (run ${f.root}/bin/gstack-upgrade to move))`); expect(sh('git rev-parse HEAD', f.root).stdout.trim()).toBe(before); expect(run(f, ['--revision', '1.2.3']).stdout).not.toContain('requested'); expect(lastLine(run(f, ['--revision', before]).stdout)).toBe('gstack: ok 1.2.3 for=autoplan'); }); + test('trailer grammar: notes ride only on ok, in the fixed order requested, pins, latest; a stale FETCH_HEAD without the wrapper\'s env adds no latest note', () => { + const f = fixture(); + const all = run(f, ['--revision', 'v9.9.9.9'], { STUB_PINS_SKIP: 'planning-only; repo pins differ: x', GSTACK_CAPY_REMOTE_VERSION: '1.10.0' }); + expect(all.status, all.stdout + all.stderr).toBe(0); + expect(lastLine(all.stdout)).toBe(`gstack: ok 1.2.3 for=autoplan (requested: v9.9.9.9 (run ${f.root}/bin/gstack-upgrade to move); pins: skipped (planning-only); latest: 1.10.0 available)`); + expect(lastLine(run(f, [], { STUB_PINS_SKIP: 'no project: /x is not inside a git repository; pass --project ' }).stdout)).toBe('gstack: ok 1.2.3 for=autoplan (pins: skipped (no project))'); + expect(lastLine(run(f, [], { GSTACK_CAPY_REMOTE_VERSION: '1.2.3' }).stdout)).toBe('gstack: ok 1.2.3 for=autoplan'); + expect(lastLine(run(f, [], { GSTACK_CAPY_REMOTE_VERSION: '1.0.9' }).stdout)).toBe('gstack: ok 1.2.3 for=autoplan'); + sh(`git update-ref FETCH_HEAD HEAD && printf '%s\\t\\tbranch main\\n' "$(git rev-parse HEAD)" > .git/FETCH_HEAD`, f.root); + write(path.join(f.root, 'VERSION'), '1.2.3\n'); + expect(lastLine(run(f, ['--check']).stdout)).toBe('gstack: ok 1.2.3 for=autoplan'); + const failed = run(f, ['--revision', 'v9.9.9.9'], { STUB_DOCTOR_FAIL: '1', STUB_PINS_SKIP: 'planning-only; x', GSTACK_CAPY_REMOTE_VERSION: '9.0.0' }); + expect(lastLine(failed.stdout)).toBe('gstack: fail codex'); + }); + test('--root clones when the directory is absent and refuses a non-gstack directory', () => { const f = fixture(); const clone = path.join(f.base, 'clone'); diff --git a/test/gstack-doctor.test.ts b/test/gstack-doctor.test.ts index 09b184985..62109098d 100644 --- a/test/gstack-doctor.test.ts +++ b/test/gstack-doctor.test.ts @@ -863,4 +863,25 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => { expect(parse(f, []).line('runtime')).toContain(`; ${shim} is not gstack's shim (no marker), left untouched: launch with the GSTACK_LAUNCH line`); expect(parse(f, [], { GSTACK_BUN_INSTALL_VERSION: '1.4.2' }).line('runtime')).toContain('; install version 1.4.2 from GSTACK_BUN_INSTALL_VERSION'); }); + + test('A-a: the Capy skill\'s dispatch examples (fail revision, fail runtime,pins, fail pins) match the real failure heads, unchanged', () => { + const skill = fs.readFileSync(path.join(REPO, 'capy/skill/gstack/SKILL.md'), 'utf8'); + const step = skill.split('\n').find(l => l.includes('If the trailer names `revision`, `runtime` or `pins`'))!; + const examples = [...step.matchAll(/`(gstack: fail [^`]+)`/g)].map(m => m[1]); + expect(examples).toEqual(['gstack: fail revision …', 'gstack: fail runtime,pins', 'gstack: fail pins']); + const f = makeFixture(); + const project = path.join(f.base, 'project'); + write(path.join(project, '.bun-version'), '1.4.2\n'); + const real: Record = { + revision: parse(f, ['--require', 'claude', '--min-revision', '99.0.0.0']).trailer, + 'runtime,pins': parse(f, ['--require', 'claude', '--project', project], { STUB_BUN_VERSION: '1.3.14' }).trailer, + pins: parse(f, ['--require', 'claude', '--project', project], { STUB_BUN_VERSION: '1.4.3' }).trailer, + }; + for (const example of examples) { + const ids = example.slice('gstack: fail '.length).replace(/ …$/, ''); + const head = real[ids]; + expect(head === `gstack: fail ${ids}` || head.startsWith(`gstack: fail ${ids} — `), `${example} vs ${head}`).toBe(true); + expect(head.match(/^gstack: fail ([a-z,-]+)/)?.[1].split(',')).toEqual(ids.split(',')); + } + }); });