diff --git a/bin/gstack-install-verdict.sh b/bin/gstack-install-verdict.sh index 73ad365cf..23661ed4b 100644 --- a/bin/gstack-install-verdict.sh +++ b/bin/gstack-install-verdict.sh @@ -18,7 +18,9 @@ # and named in the GSTACK_LAUNCH line as GSTACK_INSTALL_VERDICT=. It is # the one record the bun shim, lib/pregate/runtime.ts and admission read. # Readers fall back to the schema 1 file keyed by the checkout alone, -# /install-verdicts/.json. +# /install-verdicts/.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). # # Admission (gstack-skill-start, after SESSION_KIND): an unattended session, # or an explicit GSTACK_SESSION_KIND=spawned session with GSTACK_CAPY=1, @@ -129,7 +131,7 @@ gstack_install_admission() { src=$(gstack_source_root "$bin") || src="" project=$(gstack_install_project "$src") file=$(gstack_install_verdict_path "${_GH:-$HOME/.gstack}" "$src" "$project") - [ -f "$file" ] || [ -z "${GSTACK_INSTALL_VERDICT:-}" ] || file="$GSTACK_INSTALL_VERDICT" + { [ -f "$file" ] && ! gstack_doctor_install_profile "$(gstack_install_verdict_field "$file" for)"; } || [ ! -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" diff --git a/changelog.d/gst-1161-install-verdict.md b/changelog.d/gst-1161-install-verdict.md new file mode 100644 index 000000000..43a5abfb3 --- /dev/null +++ b/changelog.d/gst-1161-install-verdict.md @@ -0,0 +1,3 @@ +### Fixed + +- **/autoplan no longer stalls after a review phase when its reviewers share your machine (GST-1161).** A session installed from the skill's own folder gets an install verdict with no project, and its `GSTACK_LAUNCH` line points admission at that file. When a reviewer on the same machine ran `install.sh --for review-voice` from the project, it wrote a review-voice verdict keyed by that project. Admission read the project's file first, so the next `gstack-autoplan submit` refused with INSTALL_UNVERIFIED ("certified for=review-voice") until `install.sh --for autoplan` was run again. Both admission readers (`lib/install-verdict.ts` and `bin/gstack-install-verdict.sh`) now skip a review-voice verdict at the project path when the session's launched `GSTACK_INSTALL_VERDICT` exists, since a review-voice verdict can't admit any workflow. With no launched verdict, the review-voice refusal and its fix line are unchanged. `scripts/install.sh` and its user-scope copy don't change. diff --git a/lib/install-verdict.ts b/lib/install-verdict.ts index cf84f3aec..f9599a477 100644 --- a/lib/install-verdict.ts +++ b/lib/install-verdict.ts @@ -6,7 +6,10 @@ * * Verdict file: /install-verdicts//.json, * then the path the GSTACK_LAUNCH line names as GSTACK_INSTALL_VERDICT, then - * the schema 1 file keyed by the checkout alone (.json). + * the schema 1 file keyed by the checkout alone (.json). An + * install-profile verdict (for=review-voice) at the first path yields to the + * launched GSTACK_INSTALL_VERDICT, so a reviewer's install on a shared machine + * never shadows the session's own verdict (GST-1161). * The project is the working directory's git toplevel unless that is the * checkout itself. It is the one record the bun shim, the Bash twin, * lib/pregate/runtime.ts and admission read. @@ -102,11 +105,12 @@ export function installProject(checkout: string, cwd: string = process.cwd()): s try { const real = fs.realpathSync(top); return real === checkout ? null : real; } catch { return null; } } -/** The verdict file admission reads: (checkout, project), then GSTACK_INSTALL_VERDICT, then the schema 1 file. */ +/** The verdict file admission reads: (checkout, project), then GSTACK_INSTALL_VERDICT, then the schema 1 file. An install-profile verdict at (checkout, project) admits no workflow, so the launched GSTACK_INSTALL_VERDICT outranks it (GST-1161). */ export function findInstallVerdict(checkout: string, project: string | null, env: Env): string { const stateRoot = resolveStateRoot(env); const keyed = installVerdictPath(checkout, stateRoot, project); - if (fs.existsSync(keyed)) return keyed; + if (fs.existsSync(keyed) && !INSTALL_PROFILES.includes(readInstallVerdict(keyed)?.for ?? '')) return keyed; + if (fs.existsSync(keyed) && (!env.GSTACK_INSTALL_VERDICT || !fs.existsSync(env.GSTACK_INSTALL_VERDICT))) return keyed; if (env.GSTACK_INSTALL_VERDICT && fs.existsSync(env.GSTACK_INSTALL_VERDICT)) return env.GSTACK_INSTALL_VERDICT; const legacy = legacyInstallVerdictPath(checkout, stateRoot); return fs.existsSync(legacy) ? legacy : keyed; diff --git a/test/install-verdict.test.ts b/test/install-verdict.test.ts index 9e5c281a7..efeb1122f 100644 --- a/test/install-verdict.test.ts +++ b/test/install-verdict.test.ts @@ -134,6 +134,20 @@ describe('install verdict keyed by (checkout, project): both twins', () => { expect(future.detail).toContain('unsupported schema_version'); }); + test('GST-1161: a review-voice install on a shared machine never shadows the verdict the session launched with', () => { + const sr = tmp(); + const proj = project('evals'); + const launched = writeVerdict(sr, { project: '', bun_path: BUN, bun_version: '1.4.2', schema_version: 2, for: 'autoplan' }); + const env = { GSTACK_STATE_ROOT: sr, GSTACK_INSTALL_VERDICT: launched }; + expect(both('autoplan', proj, env)).toMatchObject({ refuse: false, status: 'ok' }); + writeVerdict(sr, { project: proj, bun_path: BUN, bun_version: '1.4.2', schema_version: 2, for: 'review-voice', + required: ['patch', 'runtime', 'helpers', 'state-root', 'privacy', 'cores', 'revision'], trailer: 'gstack: ok 9.9.9.9 for=review-voice' }); + expect(both('autoplan', proj, env)).toMatchObject({ refuse: false, status: 'ok' }); + const alone = both('autoplan', proj, { GSTACK_STATE_ROOT: sr }); + expect(alone).toMatchObject({ refuse: true, status: 'verdict', code: 'INSTALL_UNVERIFIED' }); + expect(alone.detail).toContain('certified for=review-voice'); + }); + test('a running Bun other than the recorded one refuses with BUN_RESOLUTION_DRIFT and names both', () => { const sr = tmp(); const a = project('a');