diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a206b47..073804273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 670aa09c5..a01fb7593 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.94.42.0 +1.94.43.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 1fb823e36..83c7bff9e 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -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, diff --git a/bin/gstack-evidence b/bin/gstack-evidence index d87f76e28..54d0acd73 100755 --- a/bin/gstack-evidence +++ b/bin/gstack-evidence @@ -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 -- "); return 2; } - const wtree = currentWtree(); - if (!wtree) { console.log("NOT_PASSED: no working-tree fingerprint"); return 1; } let records: EvidenceRecord[] = []; try { records = readJsonl(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}`); diff --git a/package.json b/package.json index 7d3d680df..ace2477a6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/pregate-guards.test.ts b/test/pregate-guards.test.ts index 6bf2153b9..3edc4595e 100644 --- a/test/pregate-guards.test.ts +++ b/test/pregate-guards.test.ts @@ -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): 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 = {}) => 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); diff --git a/test/spec-quality-gate-secret-sink.test.ts b/test/spec-quality-gate-secret-sink.test.ts index 011c604ab..eef39b142 100644 --- a/test/spec-quality-gate-secret-sink.test.ts +++ b/test/spec-quality-gate-secret-sink.test.ts @@ -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 }); });