v1.94.43.0 fix(pregate): gstack-evidence passed skips the tree hash with no ledger row; cheaper setup for two 5 s test cases (#3952)

* fix: gstack-evidence passed skips the tree hash with no ledger row; two local 5 s test cases get cheaper setup (GSTA-262)

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

* chore: changelog and TODOS

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 22:51:42 +00:00
1 parent e9b40bbd82
commit 913aca6ac0
7 files changed
+27 -21

No files matched your search

+7
View File
@@ -1,5 +1,12 @@
# Changelog
## [1.94.43.0] - 2026-11-03
### Fixed
- **`gstack-evidence passed` skips the tree hash when the ledger has nothing to match.** Before pregate runs a guard, it asks `gstack-evidence passed` whether that exact command already passed at this tree (#3929). Each check hashed the working tree (`gstack-wtree`) before it looked at the ledger. Usually the ledger has no row for the command at all, so that hash was wasted. Each check took 0.19 s idle and 0.74 to 0.94 s on a busy 4-core machine, once per guard. Now the ledger is read first, and the tree is hashed only when a local row names the command. The answer is the same. Only the no-row message drops its "at tree …" suffix.
- **Two local test cases no longer run out of time at Bun's default 5 s.** `test/pregate-guards.test.ts`'s guard-timeout case ran two full pregates, each waiting out a real 1 s guard timeout. It is now two cases, one with several guards together and one with a lone timed-out guard. Its fixture also writes the manifest into the base commit instead of making a second commit. On a 4-core machine it went from 4.1 s to 2.0 s and 1.75 s idle. With every core busy (busy loops at nice 10), it went from 0 of 3 runs passing to 10 of 10. `test/spec-quality-gate-secret-sink.test.ts` rendered every host's skill docs in `beforeAll` but reads only the claude and codex output. It now renders just those two, which are byte-identical to the full render: 1.38 s down to 0.37 s. With 16 busy loops, that hook went from timing out in 3 of 3 runs to passing 3 of 3. That is the hook timeout the macOS named-regressions job hit on #3926.
## [1.94.42.0] - 2026-11-03
### Tests
+1 -1
View File
@@ -1 +1 @@
1.94.42.0
1.94.43.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.94.42.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.94.43.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,
+5 -3
View File
@@ -653,12 +653,14 @@ function cmdPassed(argv: string[]): number {
const sep = argv.indexOf("--");
const command = sep < 0 ? "" : argv.slice(sep + 1).join(" ");
if (!command || sep !== 0) { console.error("usage: gstack-evidence passed -- <command>"); return 2; }
const wtree = currentWtree();
if (!wtree) { console.log("NOT_PASSED: no working-tree fingerprint"); return 1; }
let records: EvidenceRecord[] = [];
try { records = readJsonl<EvidenceRecord>(ledgerPath().file); } catch { records = []; }
const cmdSha = sha256(command);
const newest = records.findLast((r) => r.cmd_sha256 === cmdSha && r.wtree === wtree && r.source === undefined);
const local = records.filter((r) => r.cmd_sha256 === cmdSha && r.source === undefined);
if (!local.length) { console.log("NOT_PASSED: no local ledger row for this command"); return 1; }
const wtree = currentWtree();
if (!wtree) { console.log("NOT_PASSED: no working-tree fingerprint"); return 1; }
const newest = local.findLast((r) => r.wtree === wtree);
if (!newest) { console.log(`NOT_PASSED: no local ledger row for this command at tree ${wtree.slice(0, 12)}`); return 1; }
if (!countedPass(newest, 24)) { console.log(`NOT_PASSED: the newest row (label=${newest.label} ts=${newest.ts}) is not a counted pass within 24 h under this runtime`); return 1; }
console.log(`PASSED: label=${newest.label} source=local ts=${newest.reused_from ?? newest.ts}`);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.94.42",
"version": "1.94.43",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+6 -11
View File
@@ -30,7 +30,7 @@ import * as path from 'node:path';
import { afterEach } from 'bun:test';
import { EXIT } from '../lib/headless-artifacts';
import { capyHost, type CapyHost } from './helpers/capy-host';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
const hosts: CapyHost[] = [];
@@ -50,16 +50,8 @@ const SERIAL_TEST = "const test = require('node:test');\ntest('serial', () => {}
/** The base branch gains the manifest, its check script and the given pregate config. */
function guardedFixture(pregate: Record<string, unknown>): FixtureRepo {
const repo = makeFixtureRepo({ pregate });
const repo = makeFixtureRepo({ pregate, baseFiles: { 'scripts/check-serial.js': CHECK_SERIAL, 'scripts/serial-files.tsv': 'path\treason\n' } });
repos.push(repo);
repo.git('checkout', '-q', 'main');
write(repo.work, 'scripts/check-serial.js', CHECK_SERIAL);
write(repo.work, 'scripts/serial-files.tsv', 'path\treason\n');
repo.git('add', '-A');
repo.git('commit', '-q', '-m', 'serial manifest');
repo.git('push', '-q', 'origin', 'main');
repo.git('checkout', '-q', 'feature');
repo.git('merge', '-q', '--ff-only', 'main');
return repo;
}
const pregate = (repo: FixtureRepo, args: string[], env: Record<string, string> = {}) => runBin(repo, 'gstack-pregate', ['--base', 'main', ...args], env);
@@ -92,7 +84,7 @@ describe('guards (CD1)', () => {
expect(r.out).toContain('GSTACK_RESULT: skill=pregate status=incomplete');
});
test('each guard runs in its own process with its timeout: a failing guard does not stop the next, a timed-out guard is incomplete, never pass', () => {
test('each guard runs in its own process with its timeout: a failing guard does not stop the next, a timed-out guard never passes', () => {
const repo = guardedFixture({ ...BASE_PREGATE, guards: [
{ id: 'first', when: ['lib/**'], command: 'echo first-ran; exit 3' },
{ id: 'second', when: ['lib/**'], command: 'touch ../second-ran' },
@@ -108,6 +100,9 @@ describe('guards (CD1)', () => {
expect(row.status).toBe('fail');
expect(row.detail).toContain('first');
expect(row.detail).not.toContain('second');
});
test('a timed-out guard with no failing guard beside it is incomplete, never pass', () => {
const slowOnly = guardedFixture({ ...BASE_PREGATE, guards: [{ id: 'slow', when: ['lib/**'], command: 'sleep 20', timeout_s: 1 }] });
commit(slowOnly, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
expect(pregate(slowOnly, ['--allow-repo-commands', '--stage', 'preflight', '--only', 'guards']).status).toBe(EXIT.fail);
+6 -4
View File
@@ -9,10 +9,12 @@ let output: string;
beforeAll(() => {
output = mkdtempSync(join(tmpdir(), 'gstack-spec-gate-render-'));
const rendered = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--out-dir', output], {
cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000,
});
if (rendered.exitCode !== 0) throw new Error(rendered.stderr.toString());
for (const host of ['claude', 'codex']) {
const rendered = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', host, '--out-dir', output], {
cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000,
});
if (rendered.exitCode !== 0) throw new Error(rendered.stderr.toString());
}
});
afterAll(() => { if (output) rmSync(output, { recursive: true, force: true }); });