mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 05:56:37 +01:00
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>
This commit is contained in:
1 parent
128dd06f09
commit
4729f4752b
8 files changed
+77
-8
No files matched your search
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
+19
-2
@@ -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"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
# one checkout and one project (the caller's git toplevel, or none):
|
||||
# <state root>/install-verdicts/<sha256(checkout realpath)>/<sha256(project realpath | "none")>.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
|
||||
|
||||
+10
-1
@@ -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 <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
|
||||
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
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user