fix(install): a child's install for another workflow no longer drops /ship's install verdict (GST-1233)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-03 15:54:43 +00:00
1 parent 1ccab0d8bc
commit 8e18441bf1
8 files changed
+152 -53

No files matched your search

+5 -1
View File
@@ -429,7 +429,9 @@ verdict_kept() {
# An ok run keeps what an earlier ok install certified for this checkout, project, HEAD and Bun,
# so one machine serving /ship and its docs step stays admitted for both (#3561): required is
# the union, pins count as planning-only only when both runs skipped them, and for= lists every
# workflow (comma-separated). A failing run, another HEAD or another Bun replaces the verdict.
# workflow (comma-separated). A failing run, another HEAD or another Bun replaces the verdict, so
# each run also records its own workflow's verdict at <verdict>.for/<workflow>.json, which admission
# falls back to: a later install for another workflow never drops this one (GST-1233).
record_verdict() { # TRAILER OK — --check keeps the recorded resolution (drift is reported, never re-recorded)
local v_for="${FOR:-}" v_req v_plan e_for id
if verdict_kept; then
@@ -440,6 +442,8 @@ record_verdict() { # TRAILER OK — --check keeps the recorded resolution (drift
else set -- "$1" "$2" "$RES_BIN" "$RES_VER" "$RES_RULE" "${BUN_INSTALL:-}"; fi
v_req=$(printf '%s\n' "${OUT:-}" | sed -n 's/^required: //p' | head -1)
v_plan=$(printf '%s\n' "${OUT:-}" | grep -q '^SKIP pins (planning-only' && echo 1 || echo 0)
case "$FOR" in ''|*[!A-Za-z0-9._-]*) ;; *) [ "$PROFILE" -eq 1 ] || gstack_install_verdict_write "${VERDICT%.json}.for/$FOR.json" "$ROOT_REAL" "$HEAD_SHA" "$FOR" "$v_req" "$v_plan" "$2" "$1" "${MIN_REVISION:-}" "$LAUNCH_DIR" \
"$PROJECT_REAL" "$PIN_EXACT" "$3" "$4" "$5" "$6" "$NODE_BIN" "$NODE_VER" "$NODE_RULE" || say "could not write the install verdict ${VERDICT%.json}.for/$FOR.json" ;; esac
if [ "$2" = 1 ] && [ "$PROFILE" -eq 0 ] && [ -f "$VERDICT" ] && [ "$(gstack_install_verdict_field "$VERDICT" ok)" = true ] \
&& [ "$(gstack_install_verdict_field "$VERDICT" head)" = "$HEAD_SHA" ] && [ "$(gstack_install_verdict_field "$VERDICT" checkout)" = "$ROOT_REAL" ] \
&& [ "$(gstack_install_verdict_field "$VERDICT" bun_path)" = "$3" ] && e_for=$(gstack_install_verdict_field "$VERDICT" for) \
+66 -45
View File
@@ -21,6 +21,9 @@
# <state root>/install-verdicts/<sha256(checkout realpath)>.json. An install
# profile's verdict (for=review-voice) at the keyed path yields to the launched
# GSTACK_INSTALL_VERDICT, so a reviewer's install never shadows it (GST-1161).
# Each workflow install also writes its own record, <verdict>.for/<workflow>.json,
# which admission tries when the shared verdict does not admit that workflow, so
# another workflow's later install never drops it (GST-1233).
#
# Admission (gstack-skill-start, after SESSION_KIND): an unattended session,
# or an explicit GSTACK_SESSION_KIND=spawned session with GSTACK_CAPY=1,
@@ -109,12 +112,62 @@ gstack_install_verdict_field() {
esac
}
# _gstack_ia_check — the verdict checks gstack_install_admission runs on $file
# for $skill (its locals: src, project, where, found): sets detail (empty when
# the verdict admits), fix, code, trailer and, for a Bun drift, _ia_status.
_gstack_ia_check() {
detail=""
code=INSTALL_UNVERIFIED
fix="run the Capy skill installer (scripts/install.sh --for $skill) on this machine, then launch with the GSTACK_LAUNCH line it prints"
if [ ! -f "$file" ]; then
detail="no install verdict at $file for $src"
fix="run the Capy skill installer (scripts/install.sh --for $skill) from ${project:-this directory}: the verdict is keyed by the project directory the installer runs in, so a run from the skill's own folder does not cover ${project:-it}; then launch with the GSTACK_LAUNCH line it prints"
else
trailer=$(gstack_install_verdict_field "$file" trailer)
v_head=$(gstack_install_verdict_field "$file" head)
head=$(git -C "$src" rev-parse HEAD 2>/dev/null)
v_schema=$(gstack_install_verdict_field "$file" schema_version)
if { [ -z "$(gstack_install_verdict_field "$file" ok)" ] && [ -z "$v_schema" ]; } || { [ "$v_schema" != 1 ] && [ "$v_schema" != 2 ]; }; then
detail="the install verdict at $file is unreadable or has an unsupported schema_version; rerun the installer"
elif [ "$(gstack_install_verdict_field "$file" ok)" != true ]; then
detail="the install verdict says: $trailer"
elif [ "$(gstack_install_verdict_field "$file" checkout)" != "$src" ]; then
detail="the install verdict is for checkout $(gstack_install_verdict_field "$file" checkout), not $src"
elif [ -z "$head" ] || [ "$v_head" != "$head" ]; then
detail="the install verdict is for another HEAD (${v_head:0:12}; $src is at ${head:0:12})"
elif [ "$(gstack_install_verdict_field "$file" pins_planning_only)" = true ] && ! gstack_doctor_skill_pins_advisory "$skill"; then
detail="the install verdict is planning-only (pins skipped for $(gstack_install_verdict_field "$file" for)) and $skill runs the repo's tests"
elif [ "$(gstack_install_verdict_field "$file" schema_version)" != 1 ] && ! gstack_doctor_skill_pins_advisory "$skill" \
&& v_project=$(gstack_install_verdict_field "$file" project) && [ "$v_project" != "$project" ]; then
detail="the install verdict checked ${v_project:-no project}, not ${project:-this directory (no project)}, and $skill runs the repo's tests"
fix="run the Capy skill installer (scripts/install.sh --for $skill) from ${project:-this directory}, then launch with the GSTACK_LAUNCH line it prints"
else
for id in $(gstack_doctor_skill_requires "$skill" 2>/dev/null); do
case " $(gstack_install_verdict_field "$file" required) " in *" $id "*) ;; *) detail="the install verdict certified for=$(gstack_install_verdict_field "$file" for), which does not cover $id that $skill needs"; break ;; esac
done
if [ -n "$detail" ] && gstack_doctor_install_profile "$(gstack_install_verdict_field "$file" for)"; then
detail="$detail ($(gstack_install_verdict_field "$file" for) certifies only the read-only helpers)"
fix="run GSTACK_CAPY_ROOT=$src bash $src/capy/skill/gstack/scripts/install.sh --for $skill (upgrades this checkout in place: ./setup and host profiles, no second clone), then launch with the GSTACK_LAUNCH line it prints"
fi
fi
v_bun=$(gstack_install_verdict_field "$file" bun_path)
if [ -z "$detail" ] && [ -n "$v_bun" ]; then
running=$(gstack_realpath "$where" 2>/dev/null)
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -p process.execPath </dev/null 2>/dev/null | head -1)
if [ -n "$running" ] && [ "$running" != "$v_bun" ]; then
_ia_status=runtime code=BUN_RESOLUTION_DRIFT
detail="bun $found at $running is running, but the install verdict recorded bun $(gstack_install_verdict_field "$file" bun_version) at $v_bun"
fix="launch with the GSTACK_LAUNCH line (PATH=$(dirname "$v_bun"):\$PATH), or re-run the Capy skill installer with --check (scripts/install.sh --check --for $skill) to see what changed"
fi
fi
fi
}
# gstack_install_admission BIN_DIR SKILL KIND — sets _ia_applies (0|1),
# _ia_refuse (0|1), _ia_warn (0|1), _ia_status (ok|runtime|verdict),
# _ia_line (the INSTALL: or INSTALL_VERDICT: line to print, may be empty).
# Needs gstack-bun-version.sh and gstack-doctor-components.sh sourced.
gstack_install_admission() {
local bin="$1" skill="$2" kind="$3" hard=0 capy=0 src where found detail="" fix="" code="" file trailer v_head head id project v_project running v_bun
local bin="$1" skill="$2" kind="$3" hard=0 capy=0 src where found detail="" fix="" code="" file trailer v_head head id project v_project running v_bun v_schema _ia_own _ia_m_file _ia_m_detail _ia_m_fix _ia_m_code _ia_m_status _ia_m_trailer
_ia_applies=0 _ia_refuse=0 _ia_warn=0 _ia_status=ok _ia_line=""
case "$kind" in unattended|spawned) _ia_applies=1 ;; *) return 0 ;; esac
[ "${GSTACK_CAPY:-}" = 1 ] && capy=1
@@ -136,51 +189,19 @@ gstack_install_admission() {
esac
[ -f "$file" ] || [ ! -f "${GSTACK_INSTALL_VERDICT:-}" ] || file="$GSTACK_INSTALL_VERDICT"
[ -f "$file" ] || file=$(gstack_install_verdict_legacy_path "${_GH:-$HOME/.gstack}" "$src")
code=INSTALL_UNVERIFIED
fix="run the Capy skill installer (scripts/install.sh --for $skill) on this machine, then launch with the GSTACK_LAUNCH line it prints"
if [ ! -f "$file" ]; then
detail="no install verdict at $file for $src"
fix="run the Capy skill installer (scripts/install.sh --for $skill) from ${project:-this directory}: the verdict is keyed by the project directory the installer runs in, so a run from the skill's own folder does not cover ${project:-it}; then launch with the GSTACK_LAUNCH line it prints"
else
trailer=$(gstack_install_verdict_field "$file" trailer)
v_head=$(gstack_install_verdict_field "$file" head)
head=$(git -C "$src" rev-parse HEAD 2>/dev/null)
v_schema=$(gstack_install_verdict_field "$file" schema_version)
if { [ -z "$(gstack_install_verdict_field "$file" ok)" ] && [ -z "$v_schema" ]; } || { [ "$v_schema" != 1 ] && [ "$v_schema" != 2 ]; }; then
detail="the install verdict at $file is unreadable or has an unsupported schema_version; rerun the installer"
elif [ "$(gstack_install_verdict_field "$file" ok)" != true ]; then
detail="the install verdict says: $trailer"
elif [ "$(gstack_install_verdict_field "$file" checkout)" != "$src" ]; then
detail="the install verdict is for checkout $(gstack_install_verdict_field "$file" checkout), not $src"
elif [ -z "$head" ] || [ "$v_head" != "$head" ]; then
detail="the install verdict is for another HEAD (${v_head:0:12}; $src is at ${head:0:12})"
elif [ "$(gstack_install_verdict_field "$file" pins_planning_only)" = true ] && ! gstack_doctor_skill_pins_advisory "$skill"; then
detail="the install verdict is planning-only (pins skipped for $(gstack_install_verdict_field "$file" for)) and $skill runs the repo's tests"
elif [ "$(gstack_install_verdict_field "$file" schema_version)" != 1 ] && ! gstack_doctor_skill_pins_advisory "$skill" \
&& v_project=$(gstack_install_verdict_field "$file" project) && [ "$v_project" != "$project" ]; then
detail="the install verdict checked ${v_project:-no project}, not ${project:-this directory (no project)}, and $skill runs the repo's tests"
fix="run the Capy skill installer (scripts/install.sh --for $skill) from ${project:-this directory}, then launch with the GSTACK_LAUNCH line it prints"
else
for id in $(gstack_doctor_skill_requires "$skill" 2>/dev/null); do
case " $(gstack_install_verdict_field "$file" required) " in *" $id "*) ;; *) detail="the install verdict certified for=$(gstack_install_verdict_field "$file" for), which does not cover $id that $skill needs"; break ;; esac
done
if [ -n "$detail" ] && gstack_doctor_install_profile "$(gstack_install_verdict_field "$file" for)"; then
detail="$detail ($(gstack_install_verdict_field "$file" for) certifies only the read-only helpers)"
fix="run GSTACK_CAPY_ROOT=$src bash $src/capy/skill/gstack/scripts/install.sh --for $skill (upgrades this checkout in place: ./setup and host profiles, no second clone), then launch with the GSTACK_LAUNCH line it prints"
fi
fi
v_bun=$(gstack_install_verdict_field "$file" bun_path)
if [ -z "$detail" ] && [ -n "$v_bun" ]; then
running=$(gstack_realpath "$where" 2>/dev/null)
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -p process.execPath </dev/null 2>/dev/null | head -1)
if [ -n "$running" ] && [ "$running" != "$v_bun" ]; then
_ia_status=runtime code=BUN_RESOLUTION_DRIFT
detail="bun $found at $running is running, but the install verdict recorded bun $(gstack_install_verdict_field "$file" bun_version) at $v_bun"
fix="launch with the GSTACK_LAUNCH line (PATH=$(dirname "$v_bun"):\$PATH), or re-run the Capy skill installer with --check (scripts/install.sh --check --for $skill) to see what changed"
fi
fi
[ -n "$detail" ] || { _ia_line="INSTALL_VERDICT: ok $trailer"; return 0; }
_gstack_ia_check
# GST-1233: each install also keeps its own verdict per workflow, so when a
# later install for another workflow replaced the shared one, this
# workflow's own record still admits it.
_ia_own="${file%.json}.for/$skill.json"
case "$skill" in ''|*[!A-Za-z0-9._-]*) _ia_own="" ;; esac
if [ -n "$detail" ] && [ -n "$_ia_own" ] && [ -f "$_ia_own" ]; then
_ia_m_file=$file _ia_m_detail=$detail _ia_m_fix=$fix _ia_m_code=$code _ia_m_status=$_ia_status _ia_m_trailer=$trailer
file=$_ia_own _ia_status=ok
_gstack_ia_check
[ -z "$detail" ] || { file=$_ia_m_file detail=$_ia_m_detail fix=$_ia_m_fix code=$_ia_m_code _ia_status=$_ia_m_status trailer=$_ia_m_trailer; }
fi
[ -n "$detail" ] || { _ia_line="INSTALL_VERDICT: ok $trailer"; return 0; }
[ "$_ia_status" = runtime ] || _ia_status=verdict
else
return 0
+3
View File
@@ -0,0 +1,3 @@
### Fixed
- **A child's install for another workflow no longer drops /ship's install verdict on a shared machine (GST-1233).** When a /ship docs child ran `install.sh --for document-release` with a different Bun on PATH, or its install failed, it replaced the shared install verdict, and the parent's finish pregate refused with `INSTALL_UNVERIFIED` until someone reinstalled `--for ship`. Each workflow install now also writes that workflow's own verdict beside the shared one (`<verdict>.for/<workflow>.json`), and workflow admission and the pregate fall back to it with the same checks. A new HEAD still invalidates both.
+8 -1
View File
@@ -330,7 +330,14 @@ keeps the lease. `bin/gstack-lease list` shows `alive|idle|dead|stale`, and
the installer prints `the install verdict at <file> keeps for=<old> and adds
<new> (for=<list>)`, and the trailer still names this run's workflow). A
failing run, a new HEAD or another Bun replaces the verdict, and so does any
workflow install over a review-voice verdict. Admission refuses a workflow whose
workflow install over a review-voice verdict. Each workflow install (and
`--check`) also writes that workflow's own verdict beside it, at
`<verdict>.for/<workflow>.json`, with only this run's result. When the shared
verdict does not admit a workflow, admission and the pregate try the
workflow's own record with the same checks, so a later install for another
workflow on a shared machine (a /ship docs child resolving another Bun, or a
failed child install) never drops /ship's admission (GST-1233). A new HEAD
still invalidates both. Admission refuses a workflow whose
running Bun is not the one the verdict recorded
([BUN_RESOLUTION_DRIFT](troubleshooting.md#bun-resolution-drift)), and so
do the pregate's lanes and guards, which also refuse an invalid verdict
+17 -1
View File
@@ -142,7 +142,23 @@ export interface VerdictProblem { detail: string; code: ResultCodeName; fix?: st
export function verdictProblem(skill: string, gstackRoot: string, env: Env, bunPath: string, bunVersion: string, cwd?: string): VerdictProblem | null {
const src = sourceRoot(gstackRoot);
const project = installProject(src, cwd);
const file = findInstallVerdict(src, project, env);
return admittedVerdict(skill, src, findInstallVerdict(src, project, env), project, bunPath, bunVersion).problem;
}
/**
* The verdict file that admits `skill`: `file` itself, else the workflow's own record
* <verdict>.for/<skill>.json that every install also writes, so a later install for another
* workflow never drops this one (GST-1233). When neither admits, `file` with its problem.
*/
export function admittedVerdict(skill: string, src: string, file: string, project: string | null, bunPath: string, bunVersion: string): { file: string; problem: VerdictProblem | null } {
const problem = verdictFileProblem(file, skill, src, project, bunPath, bunVersion);
const own = file.replace(/\.json$/, `.for/${skill}.json`);
if (problem && /^[A-Za-z0-9._-]+$/.test(skill) && fs.existsSync(own) && !verdictFileProblem(own, skill, src, project, bunPath, bunVersion)) return { file: own, problem: null };
return { file, problem };
}
/** Why the verdict in `file` does not admit `skill`; null when it does. */
function verdictFileProblem(file: string, skill: string, src: string, project: string | null, bunPath: string, bunVersion: string): VerdictProblem | null {
const v = readInstallVerdict(file);
const problem = (detail: string): VerdictProblem => ({ detail, code: 'INSTALL_UNVERIFIED' });
if (!v && fs.existsSync(file)) return problem(`the install verdict at ${file} is unreadable or has an unsupported schema_version; rerun the installer`);
+5 -5
View File
@@ -22,7 +22,7 @@
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { findInstallVerdict, installProject, readInstallVerdict, sourceRoot, verdictProblem } from '../install-verdict';
import { admittedVerdict, findInstallVerdict, installProject, readInstallVerdict, sourceRoot } from '../install-verdict';
import { resultCodeSuffix } from '../result-codes';
import { collectPins, evaluatePins, exactPlan, parseVersion, satisfies } from '../runtime-pins';
import { resolveStateRoot } from '../state-root';
@@ -152,15 +152,15 @@ export interface LaunchRuntime {
* The Bun the lanes and guards run under, its dir first on PATH; the env is
* unchanged when nothing qualifies. Under the admission condition the install
* verdict for (checkout, project) is validated as workflow admission does
* (verdictProblem: ok, HEAD, pins, project, coverage, for /ship) and its
* (admittedVerdict: ok, HEAD, pins, project, coverage, for /ship; else /ship's own record) and its
* recorded Bun is the one launched; the gstack checkout itself binds the
* 'none' verdict (installProject).
*/
export function launchRuntime(repoRoot: string, env: NodeJS.ProcessEnv): LaunchRuntime {
const checkout = sourceRoot(GSTACK_ROOT);
const project = installProject(checkout, repoRoot);
const file = findInstallVerdict(checkout, project, env);
const recorded = readInstallVerdict(file);
const admitted = admittedVerdict('ship', checkout, findInstallVerdict(checkout, project, env), project, '', '');
const recorded = readInstallVerdict(admitted.file);
let pinsRoot: string | null = null;
try { pinsRoot = fs.realpathSync(repoRoot); } catch { /* unresolvable: no pins */ }
const { pinExact, accept } = projectBunPins(pinsRoot, env);
@@ -174,7 +174,7 @@ export function launchRuntime(repoRoot: string, env: NodeJS.ProcessEnv): LaunchR
if (env.GSTACK_CAPY !== '1' && !recorded) {
verdictLine = 'install verdict: not checked (no GSTACK_CAPY=1 and no verdict for this checkout and project)';
} else {
const problem = verdictProblem('ship', GSTACK_ROOT, env, '', '', repoRoot);
const { problem } = admitted;
if (problem) return refuse('INSTALL_UNVERIFIED', problem.detail, problem.fix ?? installer);
verdict = 'ok';
if (recorded!.bun_path) {
+30
View File
@@ -406,6 +406,36 @@ describe('gstack-capy-install', () => {
expect(admits('ship')?.detail).toBe('the install verdict certified for=document-release, which does not cover codex that ship needs');
});
// GST-1233 (TRUT-26): the union above needs the same Bun and an ok run, so a child whose shell
// resolves another Bun, or whose install fails, still replaced the shared verdict and /ship's
// finish pregate reported INSTALL_UNVERIFIED. Each workflow keeps its own verdict as well.
test('--for ship then --for document-release with another Bun or a failed run: ship stays admitted; another HEAD invalidates both', () => {
const f = fixture();
const env = { HOME: f.home, GSTACK_HOME: path.join(f.home, '.gstack-state') };
const admits = (skill: string) => verdictProblem(skill, f.root, env, '', '1.4.2', f.home);
const ship = run(f, ['--for', 'ship'], { STUB_REQUIRED: 'claude codex patch runtime pins' });
expect(ship.status, ship.stdout + ship.stderr).toBe(0);
const file = ship.stdout.match(/GSTACK_INSTALL_VERDICT=(\S+)/)![1]!;
const childBin = path.join(f.base, 'child-bin');
script(path.join(childBin, 'bun'), ['[ "$1" = --version ] && { echo 1.4.2; exit 0; }', 'exit 0']);
const docs = run(f, ['--for', 'document-release'], { STUB_REQUIRED: 'claude runtime', PATH: `${childBin}${path.delimiter}${f.env.PATH}` });
expect(docs.status, docs.stdout + docs.stderr).toBe(0);
expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toMatchObject({ for: 'document-release', bun_path: path.join(childBin, 'bun') });
expect(admits('ship')).toBeNull();
expect(admits('document-release')).toBeNull();
const failed = run(f, ['--for', 'document-release'], { STUB_REQUIRED: 'claude runtime', STUB_DOCTOR_FAIL: '1' });
expect(failed.status).not.toBe(0);
expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toMatchObject({ for: 'document-release', ok: false });
expect(admits('ship')).toBeNull();
expect(admits('document-release')?.detail).toContain('the install verdict says: gstack: fail');
sh('git -c user.email=t@t -c user.name=t commit -q --allow-empty -m next', f.root);
expect(admits('ship')?.detail).toMatch(/^the install verdict says: gstack: fail/);
const moved = run(f, ['--for', 'document-release'], { STUB_REQUIRED: 'claude runtime' });
expect(moved.status, moved.stdout + moved.stderr).toBe(0);
expect(admits('document-release')).toBeNull();
expect(admits('ship')?.detail).toBe('the install verdict certified for=document-release, which does not cover codex that ship needs');
});
test('--root clones when the directory is absent and refuses a non-gstack directory', () => {
const f = fixture();
const clone = path.join(f.base, 'clone');
+18
View File
@@ -152,6 +152,24 @@ describe('install verdict keyed by (checkout, project): both twins', () => {
expect(both('autoplan', proj, env).detail).toContain('unsupported schema_version');
});
test("GST-1233: a later install for another workflow replaced the shared verdict; both twins admit from the workflow's own record, never from a stale one", () => {
const sr = tmp();
const proj = project('truther');
const shared = writeVerdict(sr, { project: proj, bun_path: '/opt/child/bun', bun_version: '1.4.2', schema_version: 2, for: 'document-release',
required: ['claude', 'runtime'], trailer: 'gstack: ok 9.9.9.9 for=document-release' });
const own = shared.replace(/\.json$/, '.for/ship.json');
const env = { GSTACK_STATE_ROOT: sr };
const without = both('ship', proj, env);
expect(without).toMatchObject({ refuse: true, status: 'verdict', code: 'INSTALL_UNVERIFIED', detail: 'the install verdict certified for=document-release, which does not cover codex that ship needs' });
writeVerdict(sr, { project: proj, bun_path: BUN, bun_version: '1.4.2', schema_version: 2, for: 'ship', trailer: 'gstack: ok 9.9.9.9 for=ship' }, own);
expect(both('ship', proj, env)).toMatchObject({ refuse: false, status: 'ok' });
expect(bashAdmit('ship', proj, { _GH: sr, ...env }).line).toBe('INSTALL_VERDICT: ok gstack: ok 9.9.9.9 for=ship');
writeVerdict(sr, { project: proj, bun_path: BUN, bun_version: '1.4.2', schema_version: 2, for: 'ship', head: 'f'.repeat(40) }, own);
const stale = both('ship', proj, env);
expect(stale).toMatchObject({ refuse: true, status: 'verdict', code: 'INSTALL_UNVERIFIED', detail: without.detail });
expect(bashAdmit('ship', proj, { _GH: sr, ...env }).line).toBe(without.line);
});
test('a running Bun other than the recorded one refuses with BUN_RESOLUTION_DRIFT and names both', () => {
const sr = tmp();
const a = project('a');