Files
gstack/bin/gstack-checks
Garry Tanandcapy-ai[bot] 923ed3cddc v1.91.359.0 feat(verify): /verify skill and gstack-checks bin (GST-116 PR 1) (#3672)
* verify: add /verify skill and gstack-checks (GST-116 PR 1)

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

* fix: pre-landing review fixes

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

* fix: pre-landing review fixes (pass 2)

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

* fix: adversarial review fixes

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

* fix: win32 NUL discard for the Go build check; unique Health Stack names (rule 23 grant)

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

* docs: changelog fragment for /verify

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>
2026-10-31 09:54:03 +00:00

95 lines
6.3 KiB
TypeScript
Executable File

#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-checks — the build-and-verify loop's checks for /verify. Logic: lib/checks.ts.
*/
import { spawnSync } from 'node:child_process';
import { cancelActive } from '../lib/pregate/supervise';
import { MAX_FAIL_LINES, TESTS, isTrusted, ledgerPath, load, nothingToRun, readLedger, recordTrust, runChecks, status, type Loaded } from '../lib/checks';
const USAGE = `Usage: gstack-checks <detect|trust|run|status> [options]
detect list the checks run would execute and whether they are trusted (reads files only)
trust trust the current command set for this repo (checks, the pregate runner and its
selection manifest commands); any change to one of them needs trust again
run [--only a,b] [--allow-repo-commands] [--timeout <s>]
run the checks and the tests pregate selects for the change; one line per check:
CHECK <name> PASS|FAIL|ZERO_RUN|UNMAPPED|TOO_BROAD|ENV|TIMEOUT|INCOMPLETE <secs>
status READY_FOR_SHIP when the last run passed every check at this tree, else NOT_READY;
runs nothing
--base <branch> the branch pregate selects against (default: origin/HEAD's branch, else main)
Exit codes: 0 green or ready · 1 a red check or not ready · 2 an environment failure or timeout ·
3 consent required, no checks, or no pregate context · 64 usage
`;
function flag(args: string[], name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
}
function repoRoot(): string | null {
const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 15_000 });
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
}
function loadOrRefuse(root: string, args: string[], timeoutMs: number): Loaded | null {
const l = load(root, flag(args, '--base'), timeoutMs);
if ('refused' in l) { process.stderr.write(`gstack-checks: no pregate context, so tests cannot be selected: ${l.refused}\n`); return null; }
return l;
}
async function main(argv: string[]): Promise<number> {
const [verb, ...args] = argv;
if (!verb || verb === '--help' || verb === '-h') { process.stdout.write(USAGE); return verb ? 0 : 64; }
if (!['detect', 'trust', 'run', 'status'].includes(verb)) { process.stderr.write(USAGE); return 64; }
const root = repoRoot();
if (!root) { process.stderr.write('gstack-checks: not inside a git repository\n'); return 3; }
const timeoutS = Number(flag(args, '--timeout') ?? 600);
if (!Number.isFinite(timeoutS) || timeoutS <= 0) { process.stderr.write('gstack-checks: --timeout needs a positive number of seconds\n'); return 64; }
const l = loadOrRefuse(root, args, timeoutS * 1000);
if (!l) return 3;
const trusted = isTrusted(root, l.set);
const NONE = 'CHECKS: none (no gstack:verify: line, Health Stack section, manifest check, pregate policy or test file); nothing to run\n';
if (nothingToRun(l) && (verb === 'run' || verb === 'detect')) { process.stdout.write(NONE); return verb === 'run' ? 3 : 0; }
if (verb === 'detect') {
for (const c of l.checks) process.stdout.write(`CHECK ${c.name} ${c.command}${c.body ? ` (runs: ${c.body})` : ''} (${c.source})\n`);
if (!l.checks.length) process.stdout.write('CHECKS: tests only (no gstack:verify: line, Health Stack section or manifest check)\n');
const selection = Object.values(l.ctx.config.selection ?? {});
process.stdout.write(`TESTS pregate selection against ${l.ctx.baseRef} (policy ${l.ctx.configLabel}; runner ${l.ctx.config.runner ?? 'detected per file'}${selection.length ? `; selection commands: ${selection.join(' | ')}` : ''})\n`);
process.stdout.write(`TRUST ${trusted ? 'trusted' : 'untrusted (run gstack-checks trust after the user agrees, or pass --allow-repo-commands)'}\n`);
return 0;
}
if (verb === 'trust') {
recordTrust(root, l.set, l.checks, l.ctx.config);
process.stdout.write(`gstack-checks: trusted ${l.checks.length} check(s) and the pregate test commands for ${root}\n`);
return 0;
}
if (verb === 'status') {
const s = status(l, readLedger(ledgerPath(root)));
process.stdout.write(s.line + '\n');
return s.ready ? 0 : 1;
}
if (!trusted && !args.includes('--allow-repo-commands')) {
process.stderr.write(`gstack-checks: these commands are not trusted for ${root}; nothing was run (CHECKS_CONSENT_REQUIRED)\n`);
for (const c of l.checks) process.stderr.write(` ${c.name}: ${c.command}\n`);
process.stderr.write(` tests: ${l.ctx.config.runner ?? 'the runner pregate detects'} on the files pregate selects${Object.keys(l.ctx.config.selection ?? {}).length ? ' (and its selection manifest commands)' : ''}\n`);
process.stderr.write('fix: ask the user, then gstack-checks trust; or pass --allow-repo-commands when the session was launched to run them\n');
return 3;
}
const only = flag(args, '--only')?.split(',').map(s => s.trim()).filter(Boolean);
const unknown = (only ?? []).filter(n => n !== TESTS && !l.checks.some(c => c.name === n));
if (unknown.length) { process.stderr.write(`gstack-checks: unknown check(s) ${unknown.join(', ')}; known: ${[...l.checks.map(c => c.name), TESTS].join(', ')}\n`); return 64; }
for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => { cancelActive(); process.exit(signal === 'SIGINT' ? 130 : 143); });
const { outcomes, ledger } = await runChecks(l, { root, only, timeoutMs: timeoutS * 1000 });
for (const o of outcomes) {
process.stdout.write(`CHECK ${o.name} ${o.verdict} ${(o.ms / 1000).toFixed(1)}s${o.verdict === 'PASS' ? '' : ` ${o.detail}`}\n`);
if (o.verdict !== 'PASS') { for (const line of o.lines.slice(-MAX_FAIL_LINES)) process.stdout.write(` | ${line}\n`); if (o.log) process.stdout.write(` log: ${o.log}\n`); }
}
if (ledger.notCovered.length) process.stdout.write(`NOT_COVERED ${ledger.notCovered.join(' ')} (platform lanes /ship's pregate owns)\n`);
if (outcomes.every(o => o.verdict === 'PASS')) return 0;
return outcomes.some(o => o.verdict === 'FAIL' || o.verdict === 'ZERO_RUN' || o.verdict === 'UNMAPPED' || o.verdict === 'TOO_BROAD' || o.verdict === 'INCOMPLETE') ? 1 : 2;
}
main(process.argv.slice(2)).then(code => process.exit(code), e => { process.stderr.write(`gstack-checks: ${e?.stack ?? e}\n`); process.exit(1); });