v1.94.7.0 fix: a review-voice install on a shared machine never shadows the session's install verdict (GST-1161) (#3919)

* fix: a review-voice install on a shared machine never shadows the session's launched install verdict (GST-1161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: both verdict twins yield only to a readable review-voice verdict; test the missing launched file and an unknown schema; document the precedence (GST-1161 review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-03 06:40:16 +00:00
1 parent 6a370bd7ba
commit 22b6de6b76
8 files changed
+45 -9

No files matched your search

+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## [1.94.7.0] - 2026-11-03
### 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.
## [1.94.6.0] - 2026-11-03 ## [1.94.6.0] - 2026-11-03
Two opt-in Claude Code hooks so a model actually runs gstack's skills, both off by default (GST-1088). `./setup --skill-routing` adds a UserPromptSubmit hook that names the gstack skill a prompt fits, scored against each installed skill's `triggers:` with no model call. `./setup --skill-contracts` adds a Stop hook that blocks once when a skill with a `contract:` block ends without its required outputs (a report with real values, a test after the last edit, the specialist and adversarial dispatches). `gstack-skill-route --explain` shows how a prompt scores; `gstack-skill-contract check|report|waive` checks a transcript, lists misses and acknowledges a waiver. Turn either off with `gstack-config set skill_routing off` or `gstack-config set skill_contracts off`. Docs: docs/skill-routing-and-contracts.md. Two opt-in Claude Code hooks so a model actually runs gstack's skills, both off by default (GST-1088). `./setup --skill-routing` adds a UserPromptSubmit hook that names the gstack skill a prompt fits, scored against each installed skill's `triggers:` with no model call. `./setup --skill-contracts` adds a Stop hook that blocks once when a skill with a `contract:` block ends without its required outputs (a report with real values, a test after the last edit, the specialist and adversarial dispatches). `gstack-skill-route --explain` shows how a prompt scores; `gstack-skill-contract check|report|waive` checks a transcript, lists misses and acknowledges a waiver. Turn either off with `gstack-config set skill_routing off` or `gstack-config set skill_contracts off`. Docs: docs/skill-routing-and-contracts.md.
+1 -1
View File
@@ -1 +1 @@
1.94.6.0 1.94.7.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.94.6.0 — regenerate/re-copy after upgrading gstack # gstack digest v1.94.7.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows, for agent hosts without a full skill install. The full skills add workflows,
+7 -2
View File
@@ -18,7 +18,9 @@
# and named in the GSTACK_LAUNCH line as GSTACK_INSTALL_VERDICT=<path>. It is # and named in the GSTACK_LAUNCH line as GSTACK_INSTALL_VERDICT=<path>. It is
# the one record the bun shim, lib/pregate/runtime.ts and admission read. # 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, # Readers fall back to the schema 1 file keyed by the checkout alone,
# <state root>/install-verdicts/<sha256(checkout realpath)>.json. # <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).
# #
# Admission (gstack-skill-start, after SESSION_KIND): an unattended session, # Admission (gstack-skill-start, after SESSION_KIND): an unattended session,
# or an explicit GSTACK_SESSION_KIND=spawned session with GSTACK_CAPY=1, # or an explicit GSTACK_SESSION_KIND=spawned session with GSTACK_CAPY=1,
@@ -129,7 +131,10 @@ gstack_install_admission() {
src=$(gstack_source_root "$bin") || src="" src=$(gstack_source_root "$bin") || src=""
project=$(gstack_install_project "$src") project=$(gstack_install_project "$src")
file=$(gstack_install_verdict_path "${_GH:-$HOME/.gstack}" "$src" "$project") file=$(gstack_install_verdict_path "${_GH:-$HOME/.gstack}" "$src" "$project")
[ -f "$file" ] || [ -z "${GSTACK_INSTALL_VERDICT:-}" ] || file="$GSTACK_INSTALL_VERDICT" case "$(gstack_install_verdict_field "$file" schema_version 2>/dev/null)" in
1|2) if gstack_doctor_install_profile "$(gstack_install_verdict_field "$file" for)" && [ -f "${GSTACK_INSTALL_VERDICT:-}" ]; then file="$GSTACK_INSTALL_VERDICT"; fi ;;
esac
[ -f "$file" ] || [ ! -f "${GSTACK_INSTALL_VERDICT:-}" ] || file="$GSTACK_INSTALL_VERDICT"
[ -f "$file" ] || file=$(gstack_install_verdict_legacy_path "${_GH:-$HOME/.gstack}" "$src") [ -f "$file" ] || file=$(gstack_install_verdict_legacy_path "${_GH:-$HOME/.gstack}" "$src")
code=INSTALL_UNVERIFIED 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" fix="run the Capy skill installer (scripts/install.sh --for $skill) on this machine, then launch with the GSTACK_LAUNCH line it prints"
+4 -1
View File
@@ -42,7 +42,10 @@ no host profile, so a workflow started there is refused (`INSTALL: fail verdict
— … (review-voice certifies only the read-only helpers); fix: run — … (review-voice certifies only the read-only helpers); fix: run
GSTACK_CAPY_ROOT=<root> bash <root>/capy/skill/gstack/scripts/install.sh --for GSTACK_CAPY_ROOT=<root> bash <root>/capy/skill/gstack/scripts/install.sh --for
<skill> …`). A review-voice check never replaces an ok verdict that a fuller <skill> …`). A review-voice check never replaces an ok verdict that a fuller
install wrote for the same HEAD and project. The reverse is an in-place install wrote for the same HEAD and project. Nor does one run from another
project shadow the session's own verdict: when the verdict for the current
project is a review-voice one, admission reads the session's launched
`GSTACK_INSTALL_VERDICT` instead (GST-1161). The reverse is an in-place
upgrade: a reviewer later asked to run a workflow runs the same installer upgrade: a reviewer later asked to run a workflow runs the same installer
`--for <skill>`, which reuses the checkout (`upgrading the review-voice `--for <skill>`, which reuses the checkout (`upgrading the review-voice
install in place to --for <skill> (same checkout ...; no second clone)`), install in place to --for <skill> (same checkout ...; no second clone)`),
+7 -3
View File
@@ -6,7 +6,10 @@
* *
* Verdict file: <state root>/install-verdicts/<sha256(checkout realpath)>/<sha256(project realpath | "none")>.json, * Verdict file: <state root>/install-verdicts/<sha256(checkout realpath)>/<sha256(project realpath | "none")>.json,
* then the path the GSTACK_LAUNCH line names as GSTACK_INSTALL_VERDICT, then * then the path the GSTACK_LAUNCH line names as GSTACK_INSTALL_VERDICT, then
* the schema 1 file keyed by the checkout alone (<sha256(checkout)>.json). * the schema 1 file keyed by the checkout alone (<sha256(checkout)>.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 * 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, * checkout itself. It is the one record the bun shim, the Bash twin,
* lib/pregate/runtime.ts and admission read. * 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; } 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 { export function findInstallVerdict(checkout: string, project: string | null, env: Env): string {
const stateRoot = resolveStateRoot(env); const stateRoot = resolveStateRoot(env);
const keyed = installVerdictPath(checkout, stateRoot, project); 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; if (env.GSTACK_INSTALL_VERDICT && fs.existsSync(env.GSTACK_INSTALL_VERDICT)) return env.GSTACK_INSTALL_VERDICT;
const legacy = legacyInstallVerdictPath(checkout, stateRoot); const legacy = legacyInstallVerdictPath(checkout, stateRoot);
return fs.existsSync(legacy) ? legacy : keyed; return fs.existsSync(legacy) ? legacy : keyed;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "gstack", "name": "gstack",
"version": "1.94.6", "version": "1.94.7",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+18
View File
@@ -134,6 +134,24 @@ describe('install verdict keyed by (checkout, project): both twins', () => {
expect(future.detail).toContain('unsupported schema_version'); 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');
const gone = both('autoplan', proj, { GSTACK_STATE_ROOT: sr, GSTACK_INSTALL_VERDICT: path.join(sr, 'gone.json') });
expect(gone.detail).toContain('certified for=review-voice');
writeVerdict(sr, { project: proj, schema_version: 3, for: 'review-voice' });
expect(both('autoplan', proj, env).detail).toContain('unsupported schema_version');
});
test('a running Bun other than the recorded one refuses with BUN_RESOLUTION_DRIFT and names both', () => { test('a running Bun other than the recorded one refuses with BUN_RESOLUTION_DRIFT and names both', () => {
const sr = tmp(); const sr = tmp();
const a = project('a'); const a = project('a');