mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
Merge origin/main into gsta-167-review-reads; v1.91.330.0 stamp
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
commit
f85ccbc9fd
8 files changed
+144
-5
No files matched your search
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"tests": [
|
||||
"test/*tripwire*.test.ts",
|
||||
"test/*ratchet*.test.ts",
|
||||
"test/env-host-fence-conformance.test.ts",
|
||||
"test/free-fixtures.test.ts",
|
||||
"test/bun-entry-isolation.test.ts",
|
||||
"test/egress-receipt-wiring.test.ts"
|
||||
]
|
||||
}
|
||||
+12
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
## [1.91.330.0] - 2026-10-30
|
||||
|
||||
**/ship reviewers now check a suspected defect against the code it depends on before they report it or let it go.** A reviewer packet used to allow at most two more ranges of up to 200 lines, from files in the diff, in one turn. In GSTA-167's no-loss replay, 7 recorded blockers needed context outside those ranges, and no reviewer found them. Each reviewer now confirms or rules out a suspected defect by reading the callers and callees of changed functions, the tests that cover them, and the config and contracts they read. It uses grep and the read tool, at most 8 reads in at most 2 turns of parallel calls. The coverage audit keeps its own budget.
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
- `test/review-packet.test.ts`: the batched specialist, Red Team, adversarial and fix-check packets carry the rule; the coverage audit keeps its own budget; the rule names callers, callees, tests and contracts with its read and turn bound; the Capy skill and the policy match it.
|
||||
- `test/size-ledger/ship/gsta-167-review-reads.json`: 108 bytes for the longer brief, rendered three times in the ship sections.
|
||||
|
||||
## [1.91.329.0] - 2026-10-30
|
||||
|
||||
**A selected pregate now runs the whole-tree tripwires and ratchets on every code branch, so they fail before review instead of in CI.** #3592 and #3593 passed their selected gate and then failed CI on exactly these scanners, because no touched file maps to a test that reads the whole tree.
|
||||
|
||||
### Added
|
||||
- **`.gstack/always-run.json`** (`{ "tests": [globs] }`): whole-tree tests that join the `lanes` selection whenever a branch selects any test, owes a lane, or edits the list. `--explain` prints each as `<test> (always_run, .gstack/always-run.json)` and a glob that matches nothing as `<glob> matches no test file`. A docs-only branch adds nothing. A missing file means an empty list; a malformed one stops the check with a message naming the file. gstack's list names 15 tests (tripwires, ratchets, env-host fence conformance, free-fixtures, bun-entry-isolation, egress wiring); `host-conformance`, which runs `./setup`, stays off it.
|
||||
- docs/pregate.md has a section on the file and why it is separate from `pregate.json` (older readers reject unknown keys there).
|
||||
|
||||
### Tests
|
||||
- `test/pregate-always-run.test.ts`: a code change adds the scanner the import graph never selects, and not without the file; a docs-only branch adds nothing; an edit to the list runs it and owes no lane; malformed lists stop; gstack's own list names only tracked tests and includes the GST-98 scanners.
|
||||
|
||||
## [1.91.328.0] - 2026-10-30
|
||||
|
||||
**Most PRs no longer pay for a solo paid screen before the merge train runs the same cases.** `gstack-ship-queue ready` used to recommend `--screen` whenever a diff touched a skill's own cases, which is almost every PR, and the screens (16 to 19 a day at about $32 each) cost more than the train gates that rerun all 137 cases a few hours later. Of 41 full screens between Oct 20 and 29, the 3 that caught a real bug all came from diffs the selector can't narrow (an unknown dependency or a shared runtime input). The recommendation is now `--screen` only in that case; every other PR rides unscreened and the train gate's full census covers it. Because most PRs now ride unscreened, a train also stopped holding them back, so unscreened PRs fill the seats a screened PR leaves.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.328.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.330.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,
|
||||
|
||||
@@ -135,6 +135,12 @@ writes `[]`, which would clear a file without a test. A file with no nearby
|
||||
test, and a lane whose matrix a planner computes, is printed as a `TODO`.
|
||||
Review each entry before committing the file on the base branch.
|
||||
|
||||
## `.gstack/always-run.json`
|
||||
|
||||
`{ "tests": [globs] }` names whole-tree tests that scan every file of a kind (tripwires, ratchets, conformance and fixture inventories). A diff-selected run never maps a touched file to them, so they used to pass the selected gate and then fail CI (GST-98). When a branch selects any test, owes a lane, or edits this file, the `lanes` selection adds every tracked test file a glob matches. `--explain` prints each one as `<test> (always_run, .gstack/always-run.json)`, and a glob that matches no test as `<glob> matches no test file`. A docs-only branch adds nothing.
|
||||
|
||||
The list lives in its own file, not in `pregate.json`, because older pregate readers reject unknown keys there. A repository that ignores `.gstack/` (gstack does) tracks the file with `git add -f`, as it does `pregate.json`. A missing file means an empty list. A file that is not JSON, or whose `tests` is not a list of non-empty strings, stops the check with that message. gstack's own list matches 15 test files; the 13 with a seed in `scripts/free-test-durations.json` (recorded 2026-10-19) total about 13 s there, and `bun-entry-isolation` and `launch-env-echo-ratchet` have no seed yet.
|
||||
|
||||
## `gstack-pregate`
|
||||
|
||||
```
|
||||
|
||||
@@ -20,6 +20,29 @@ import { stampFileKind, versionStampOnly } from './stamp-only';
|
||||
import type { PregateContext, RemoteObligation } from './types';
|
||||
import type { Lane } from './workflows';
|
||||
|
||||
/** Whole-tree tests every selection that runs a test must include (GST-98). Kept out of pregate.json so older readers, which reject unknown keys, keep working. */
|
||||
export const ALWAYS_RUN_PATH = '.gstack/always-run.json';
|
||||
|
||||
/** The tracked test files the `tests` globs in `.gstack/always-run.json` name, and the globs that name none. No file: none. */
|
||||
export function alwaysRunTests(repoRoot: string, testFiles: Iterable<string>): { tests: string[]; unmatched: string[] } {
|
||||
const file = path.join(repoRoot, ALWAYS_RUN_PATH);
|
||||
if (!fs.existsSync(file)) return { tests: [], unmatched: [] };
|
||||
let parsed: unknown;
|
||||
try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
||||
catch (error) { throw new Error(`${ALWAYS_RUN_PATH}: not valid JSON (${(error as Error).message})`); }
|
||||
const globs = (parsed as { tests?: unknown } | null)?.tests;
|
||||
if (!Array.isArray(globs) || globs.some(g => typeof g !== 'string' || !g)) throw new Error(`${ALWAYS_RUN_PATH}: "tests" must be a list of non-empty glob strings`);
|
||||
const all = [...testFiles];
|
||||
const tests = new Set<string>();
|
||||
const unmatched: string[] = [];
|
||||
for (const glob of globs as string[]) {
|
||||
const hits = all.filter(t => matchesAny([glob], t));
|
||||
if (!hits.length) unmatched.push(glob);
|
||||
for (const t of hits) tests.add(t);
|
||||
}
|
||||
return { tests: [...tests].sort(), unmatched };
|
||||
}
|
||||
|
||||
/** A path the branch removed (deleted, or a rename's old name) that owes no lane, with why (`note`) and the files that lost their last test. */
|
||||
export interface Removed { file: string; note: string; lostCoverage: string[] }
|
||||
|
||||
@@ -210,7 +233,7 @@ export function selectLanes(ctx: PregateContext): Selection {
|
||||
const generator = ctx.registry.entries.find(e => e.command && matchesAny(e.inputs, file));
|
||||
if (generator) { provenance.push(`${file}: covered by regen ${generator.id} (registry input)`); continue; }
|
||||
// The pin files are validated by pregate itself (validateConfig / validateRegistry) on every run.
|
||||
if (file === PREGATE_CONFIG_PATH || file === GENERATED_PATH) { provenance.push(`${file}: pin file validated by pregate`); continue; }
|
||||
if (file === PREGATE_CONFIG_PATH || file === GENERATED_PATH || file === ALWAYS_RUN_PATH) { provenance.push(`${file}: pin file validated by pregate`); continue; }
|
||||
// Reached only when testsFor mapped nothing, so a Markdown file a test reads never lands here.
|
||||
if (isDocsEmptyLane(file, ctx.config.empty_lanes ?? [])) {
|
||||
emptyLane.push(file);
|
||||
@@ -246,6 +269,11 @@ export function selectLanes(ctx: PregateContext): Selection {
|
||||
}
|
||||
noLane.push(file);
|
||||
}
|
||||
if (tests.size || noLane.length || seeds.has(ALWAYS_RUN_PATH)) {
|
||||
const always = alwaysRunTests(ctx.repoRoot, [...graph.tests].filter(t => !goneSet.has(t)));
|
||||
for (const t of always.tests) if (!tests.has(t)) { tests.add(t); provenance.push(`${t} (always_run, ${ALWAYS_RUN_PATH})`); }
|
||||
for (const glob of always.unmatched) provenance.push(`${ALWAYS_RUN_PATH}: ${glob} matches no test file`);
|
||||
}
|
||||
const laneSets: Record<string, string[] | '?'> = {};
|
||||
const local = new Set<string>();
|
||||
const remote: RemoteObligation[] = [];
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.328",
|
||||
"version": "1.91.330",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* GST-167 PR 1 (GST-98): whole-tree tests named in .gstack/always-run.json join
|
||||
* every selection that runs a test, because no touched file maps to a scanner
|
||||
* that reads the whole tree. #3592 and #3593 passed their selected gate and
|
||||
* then failed CI on exactly these tests.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { ALWAYS_RUN_PATH, alwaysRunTests } from '../lib/pregate/lane-selection';
|
||||
import { listRepoFiles, isTestFile } from '../lib/pregate/import-graph';
|
||||
import { cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SCANNER = "const test = require('node:test');\ntest('scans the tree', () => {});\n";
|
||||
const repos: FixtureRepo[] = [];
|
||||
const fixture = (alwaysRun: unknown) => {
|
||||
const r = makeFixtureRepo({ baseFiles: { 'test/tree-tripwire.test.js': SCANNER, ...(alwaysRun === undefined ? {} : { [ALWAYS_RUN_PATH]: typeof alwaysRun === 'string' ? alwaysRun : JSON.stringify(alwaysRun) }) } });
|
||||
repos.push(r);
|
||||
return r;
|
||||
};
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
const lanes = (repo: FixtureRepo) => runBin(repo, 'gstack-pregate', ['--base', 'main', '--allow-repo-commands', '--only', 'lanes', '--explain']);
|
||||
|
||||
describe('always_run selection', () => {
|
||||
test('a code change adds the listed scanner the import graph never selects; without the file it is not added', () => {
|
||||
const repo = fixture({ tests: ['test/*-tripwire.test.js', 'test/missing-*.test.js'] });
|
||||
commit(repo, { 'lib/util.js': 'exports.add = (a, b) => a + b + 0;\n' });
|
||||
const r = lanes(repo);
|
||||
expect(r.status).toBe(EXIT.ok);
|
||||
expect(r.out).toContain('test/util.test.js ← lib/util.js');
|
||||
expect(r.out).toContain(`test/tree-tripwire.test.js (always_run, ${ALWAYS_RUN_PATH})`);
|
||||
expect(r.out).toContain(`${ALWAYS_RUN_PATH}: test/missing-*.test.js matches no test file`);
|
||||
|
||||
const bare = fixture(undefined);
|
||||
commit(bare, { 'lib/util.js': 'exports.add = (a, b) => a + b + 0;\n' });
|
||||
const b = lanes(bare);
|
||||
expect(b.status).toBe(EXIT.ok);
|
||||
expect(b.out).toContain('test/util.test.js ← lib/util.js');
|
||||
expect(b.out).not.toContain('always_run');
|
||||
});
|
||||
|
||||
test('a docs-only branch adds nothing; editing the list itself runs it and owes no lane', () => {
|
||||
const docs = fixture({ tests: ['test/*-tripwire.test.js'] });
|
||||
commit(docs, { 'docs/README.md': '# docs\n\nupdated\n' });
|
||||
const d = lanes(docs);
|
||||
expect(d.status).toBe(EXIT.ok);
|
||||
expect(d.out).not.toContain('always_run');
|
||||
|
||||
const list = fixture({ tests: ['test/*-tripwire.test.js'] });
|
||||
commit(list, { [ALWAYS_RUN_PATH]: JSON.stringify({ tests: ['test/tree-*.test.js'] }) });
|
||||
const l = lanes(list);
|
||||
expect(l.status).toBe(EXIT.ok);
|
||||
expect(l.out).toContain(`${ALWAYS_RUN_PATH}: pin file validated by pregate`);
|
||||
expect(l.out).toContain(`test/tree-tripwire.test.js (always_run, ${ALWAYS_RUN_PATH})`);
|
||||
expect(l.err).not.toContain('PREGATE_NO_LANE');
|
||||
});
|
||||
|
||||
test('a malformed list stops with a message naming the file and the fix', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-always-run-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(dir, '.gstack'));
|
||||
fs.writeFileSync(path.join(dir, ALWAYS_RUN_PATH), '{ not json');
|
||||
expect(() => alwaysRunTests(dir, [])).toThrow(`${ALWAYS_RUN_PATH}: not valid JSON`);
|
||||
fs.writeFileSync(path.join(dir, ALWAYS_RUN_PATH), JSON.stringify({ tests: ['ok', ''] }));
|
||||
expect(() => alwaysRunTests(dir, [])).toThrow('"tests" must be a list of non-empty glob strings');
|
||||
fs.writeFileSync(path.join(dir, ALWAYS_RUN_PATH), JSON.stringify({ tests: 'test/*.ts' }));
|
||||
expect(() => alwaysRunTests(dir, [])).toThrow('"tests" must be a list of non-empty glob strings');
|
||||
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("gstack's own list: every glob names a tracked test, and the GST-98 scanners are on it", () => {
|
||||
const tracked = listRepoFiles(ROOT).filter(isTestFile);
|
||||
const own = alwaysRunTests(ROOT, tracked);
|
||||
expect(own.unmatched).toEqual([]);
|
||||
for (const t of ['test/spawnsync-timeout-tripwire.test.ts', 'test/script-invocation-tripwire.test.ts', 'test/egress-receipt-wiring.test.ts',
|
||||
'test/env-host-fence-conformance.test.ts', 'test/free-fixtures.test.ts', 'test/bun-entry-isolation.test.ts', 'test/module-size-ratchet.test.ts']) {
|
||||
expect(own.tests).toContain(t);
|
||||
}
|
||||
expect(own.tests).not.toContain('test/host-conformance.test.ts');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user