From 4729f4752ba6aa6524f52fa49f51c28779a3ea24 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 28 Oct 2026 19:33:42 -0700 Subject: [PATCH] v1.91.304.0 fix(capy-install): an install for one workflow keeps the other current verdicts (#3561) (#3563) * fix(capy-install): an install for one workflow keeps the other current verdicts (#3561) The install verdict is one file per checkout and project, and an ok --for document-release run replaced the --for ship verdict with its narrower required list, so /ship's docs step left the session without a ship admission. An ok run at the same checkout, HEAD and Bun now merges the earlier ok verdict: required is the union, pins are planning-only only when both runs skipped them, and for= lists every workflow. A failing run, another HEAD or another Bun still replaces it. Fixes #3561 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): installs for two workflows keep both verdicts (#3561) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(capy-install): an ok install keeps the other current verdicts (#3561) 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> --- CHANGELOG.md | 10 ++++++++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bin/gstack-capy-install | 21 ++++++++++++++++++-- bin/gstack-install-verdict.sh | 4 +++- docs/capy-install.md | 11 ++++++++++- package.json | 2 +- test/gstack-capy-install.test.ts | 33 +++++++++++++++++++++++++++++++- 8 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f8090db2..f6cfab56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.91.304.0] - 2026-10-29 + +**Running the Capy installer for a second workflow on the same machine no longer takes away the first one's admission.** /ship's docs step runs `scripts/install.sh --for document-release`, and that run replaced the `--for ship` install verdict with its narrower one, so the rest of /ship refused with "the install verdict certified for=document-release, which does not cover codex that ship needs" until someone reran `--for ship` (#3561, seen on gstack-evals#13). + +### Fixed +- **`bin/gstack-capy-install`**: an ok run keeps what an earlier ok install certified for the same checkout, project, HEAD and Bun. The verdict's `required` list becomes the union, pins count as planning-only only when both runs skipped them, and `for` lists every workflow (`for=ship,document-release`). The installer says so in one line. A failing run, a new HEAD or another Bun still replaces the verdict, and the review-voice profile still never narrows a fuller one. + +### Tests +- `test/gstack-capy-install.test.ts`: `--for ship` then `--for document-release` leaves one ok verdict that admits both workflows through `verdictProblem`; a `--check --for ship` keeps it; after a new commit, `--for document-release` replaces it and ship is refused again. It failed before this change. + ## [1.91.303.0] - 2026-10-29 **`gstack-pr-shots` no longer edits your `.gitignore` in the middle of /ship, so the pregate stays fresh and the ship receipt gets written.** When a repository tracks files under `.gstack/` (`pregate.json`, `ship-policy.json`), git does not read `.gstack/` as ignored, and the browse daemon that `gstack-pr-shots` starts appended `.gstack/` to the tracked `.gitignore`. The tree changed after pregate and the receipt writer refused with PREGATE_STALE (#3560, seen on gstack-evals#13). diff --git a/VERSION b/VERSION index 184cc85e7..d38dd36b0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.303.0 +1.91.304.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 0afaf21a3..0196bed74 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.303.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.304.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-capy-install b/bin/gstack-capy-install index 62975197f..93b51b75c 100755 --- a/bin/gstack-capy-install +++ b/bin/gstack-capy-install @@ -407,15 +407,32 @@ verdict_kept() { [ "$PROFILE" -eq 1 ] && [ -f "$VERDICT" ] && [ "$(gstack_install_verdict_field "$VERDICT" ok)" = true ] \ && [ "$(gstack_install_verdict_field "$VERDICT" head)" = "$HEAD_SHA" ] && ! gstack_doctor_install_profile "$(gstack_install_verdict_field "$VERDICT" for)" } +# 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. 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 say "kept the install verdict for=$(gstack_install_verdict_field "$VERDICT" for) at $VERDICT (it covers more than $FOR; the trailer below is this check's)" return 0 fi if [ "$CHECK_ONLY" -eq 1 ] && [ -n "$REC_BUN" ]; then set -- "$1" "$2" "$REC_BUN" "$REC_VER" "$REC_RULE" "$REC_BUN_INSTALL" else set -- "$1" "$2" "$RES_BIN" "$RES_VER" "$RES_RULE" "${BUN_INSTALL:-}"; fi - gstack_install_verdict_write "$VERDICT" "$ROOT_REAL" "$HEAD_SHA" "${FOR:-}" "$(printf '%s\n' "${OUT:-}" | sed -n 's/^required: //p' | head -1)" \ - "$(printf '%s\n' "${OUT:-}" | grep -q '^SKIP pins (planning-only' && echo 1 || echo 0)" "$2" "$1" "${MIN_REVISION:-}" "$LAUNCH_DIR" \ + 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) + 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) \ + && ! gstack_doctor_install_profile "$e_for"; then + for id in $(gstack_install_verdict_field "$VERDICT" required); do + case " $v_req " in *" $id "*) ;; *) v_req="$v_req $id" ;; esac + done + [ "$(gstack_install_verdict_field "$VERDICT" pins_planning_only)" = true ] || v_plan=0 + case ",$e_for," in *",$v_for,"*) v_for="$e_for" ;; *) v_for="${e_for:+$e_for${v_for:+,}}$v_for" ;; esac + [ "$v_for" = "$e_for" ] || [ -z "$e_for" ] || say "the install verdict at $VERDICT keeps for=$e_for and adds ${FOR:-$REQUIRE} (for=$v_for)" + fi + gstack_install_verdict_write "$VERDICT" "$ROOT_REAL" "$HEAD_SHA" "$v_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" } diff --git a/bin/gstack-install-verdict.sh b/bin/gstack-install-verdict.sh index 5f553376d..73ad365cf 100644 --- a/bin/gstack-install-verdict.sh +++ b/bin/gstack-install-verdict.sh @@ -8,7 +8,9 @@ # one checkout and one project (the caller's git toplevel, or none): # /install-verdicts//.json # written atomically by bin/gstack-capy-install (install and --check) with -# schema_version 2, checkout, head, for, required[], pins_planning_only, ok, +# schema_version 2, checkout, head, for (comma-separated: an ok run at the same +# HEAD and Bun keeps the workflows an earlier ok install certified, #3561), +# required[] (their union), pins_planning_only, ok, # trailer, min_revision, launch_path, time, project, pin (the exact Bun pin), # bun_path (realpath), bun_version, bun_rule (gstack_bun_resolve), bun_install, # node_path (realpath), node_version, node_rule (gstack_node_resolve; empty diff --git a/docs/capy-install.md b/docs/capy-install.md index 1bed678f9..f98f87d6e 100644 --- a/docs/capy-install.md +++ b/docs/capy-install.md @@ -274,7 +274,16 @@ keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and honoured, the resolved Bun (`bun_path`, `bun_version`, `bun_rule`, and `bun_install` when this run downloaded it), the resolved Node when the project pins one (`node_path`, `node_version`, `node_rule`), the trailer, the remote - minimum, the launch PATH and the time. Admission refuses a workflow whose + minimum, the launch PATH and the time. An ok run (install or `--check`) at + the same checkout, project, HEAD and Bun as an earlier ok verdict keeps what + that verdict certified, so one machine stays admitted for /ship and its + `--for document-release` docs step (#3561): `required` becomes the union, + pins count as planning-only only when both runs skipped them, and the + verdict's `for` lists every workflow, comma-separated (`for=ship,document-release`; + the installer prints `the install verdict at keeps for= and adds + (for=)`, 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 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 diff --git a/package.json b/package.json index 75e44f893..da4c43471 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.303", + "version": "1.91.304", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/gstack-capy-install.test.ts b/test/gstack-capy-install.test.ts index d40b86ed4..937eaf57c 100644 --- a/test/gstack-capy-install.test.ts +++ b/test/gstack-capy-install.test.ts @@ -13,6 +13,7 @@ import { spawnSync } from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { verdictProblem } from '../lib/install-verdict'; const REPO = path.resolve(import.meta.dir, '..'); const BUN_DEFAULT = fs.readFileSync(path.join(REPO, 'bin/gstack-bun-version.sh'), 'utf8').match(/^GSTACK_BUN_DEFAULT="([^"]+)"$/m)![1]; @@ -58,7 +59,7 @@ function fixture(opts: { bun?: string; jq?: boolean } = {}) { script(path.join(root, 'setup'), ['echo "setup $*" >> "$CAPY_LOG"', 'echo "gstack ready ($2)"', '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 "doctor-node $(node --version 2>/dev/null)" >> "$CAPY_LOG"', - 'echo "required: claude codex patch"', 'echo "PASS claude ok"', 'echo "SKIP browser (lazy)"', '[ -n "${STUB_PINS_SKIP:-}" ] && echo "SKIP pins ($STUB_PINS_SKIP)"', '[ -n "${STUB_DOCTOR_EXTRA:-}" ] && echo "$STUB_DOCTOR_EXTRA"', + 'echo "required: ${STUB_REQUIRED:-claude codex patch}"', 'echo "PASS claude ok"', 'echo "SKIP browser (lazy)"', '[ -n "${STUB_PINS_SKIP:-}" ] && echo "SKIP pins ($STUB_PINS_SKIP)"', '[ -n "${STUB_DOCTOR_EXTRA:-}" ] && echo "$STUB_DOCTOR_EXTRA"', '[ "${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"', @@ -351,6 +352,36 @@ describe('gstack-capy-install', () => { expect(run(f, ['--for', 'review-voice', '--project', project]).stdout).not.toMatch(upgrading); }); + // #3561: on a shared machine /ship's docs step runs the installer --for document-release, whose + // narrower verdict replaced the --for ship one, so /ship lost admission mid-run. + test('--for ship then --for document-release: the verdict keeps both and admits both workflows; another HEAD replaces it', () => { + 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]!; + expect(admits('ship')).toBeNull(); + const docs = run(f, ['--for', 'document-release'], { STUB_REQUIRED: 'claude runtime pins' }); + expect(docs.status, docs.stdout + docs.stderr).toBe(0); + expect(docs.stdout).toContain(`gstack-capy-install: the install verdict at ${file} keeps for=ship and adds document-release (for=ship,document-release)`); + const v = JSON.parse(fs.readFileSync(file, 'utf8')); + expect(v).toMatchObject({ ok: true, for: 'ship,document-release', pins_planning_only: false }); + expect([...v.required].sort()).toEqual(['claude', 'codex', 'patch', 'pins', 'runtime']); + expect(admits('ship')).toBeNull(); + expect(admits('document-release')).toBeNull(); + expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toEqual(v); + const again = run(f, ['--check', '--for', 'ship'], { STUB_REQUIRED: 'claude codex patch runtime pins' }); + expect(again.status, again.stdout + again.stderr).toBe(0); + expect(again.stdout).not.toContain('keeps for='); + expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toMatchObject({ for: 'ship,document-release' }); + sh('git -c user.email=t@t -c user.name=t commit -q --allow-empty -m next', f.root); + const moved = run(f, ['--for', 'document-release'], { STUB_REQUIRED: 'claude runtime pins' }); + expect(moved.status, moved.stdout + moved.stderr).toBe(0); + expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toMatchObject({ for: 'document-release', required: ['claude', 'runtime', 'pins'] }); + 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');