feat(qa): helpers answer --help, and the QA eval interfaces declare it

Approved by Garry: asking gstack-qa-evidence or gstack-qa-deadline for usage
is read-only, so both helpers print usage and exit 0 on --help (the evidence
usage now names the annotation shape), and the functional and caller command
allowlists accept exactly 'bun <path>/bin/gstack-qa-{evidence,deadline} --help'.
Two CI runs failed only on that call.
This commit is contained in:
garrytan committed 2026-09-30 19:14:59 +00:00
1 parent 4643cb8550
commit ee929ff710
5 files changed
+37 -3

No files matched your search

+6 -1
View File
@@ -7,6 +7,7 @@ import { initializeWindowsReviewJob } from './claude-code-windows-job';
const MAX_MS = 2_147_483_647;
class QaDeadlineError extends Error {}
const QA_DEADLINE_USAGE = 'gstack-qa-deadline start FILE SECONDS [EARLIER_UTC] | status FILE | run FILE -- COMMAND ARGS...';
type QaCommandResult = { exitCode: number; signal: NodeJS.Signals | null; completed: boolean };
type Emit = (stream: 'stdout' | 'stderr', receipt: Record<string, unknown>, completion?: QaCommandResult) => void;
@@ -269,6 +270,10 @@ export async function qaDeadlineMain(args: string[], receiptWorker = false): Pro
return withQaReceiptOutput(receiptWorker, 'qa-deadline-receipt', receipt => '\nQA_DEADLINE ' + JSON.stringify({ guard: 'qa-deadline', ...receipt }) + '\n', async emit => {
try {
const [action, file, ...rest] = args;
if (action === '--help' && args.length === 1) {
emit('stdout', { event: 'help', usage: QA_DEADLINE_USAGE });
return 0;
}
if (action === 'start' && file && (rest.length === 1 || rest.length === 2)) {
const status = qaDeadlineStatus(startQaDeadline(file, rest[0], rest[1]));
emit('stdout', { event: 'start', ...status });
@@ -283,7 +288,7 @@ export async function qaDeadlineMain(args: string[], receiptWorker = false): Pro
if (process.platform === 'win32' && !receiptWorker) return await runWindowsWorker(args, emit);
return await runQaDeadlineCommand(file, rest[1], rest.slice(2), emit);
}
throw new QaDeadlineError('Usage: gstack-qa-deadline start FILE SECONDS [EARLIER_UTC] | status FILE | run FILE -- COMMAND ARGS...');
throw new QaDeadlineError(`Usage: ${QA_DEADLINE_USAGE}`);
} catch (error) {
emit('stderr', { event: 'error', message: error instanceof QaDeadlineError ? error.message : 'Deadline guard failed' });
return 2;
+7 -1
View File
@@ -277,12 +277,18 @@ function materialize(root: string, source: string) {
next: 'Include every reportLinks entry in the Markdown report.' };
}
const QA_EVIDENCE_USAGE = 'capture ROOT ID [--public] --deadline FILE|--timeout-ms MS -- COMMAND ARGS | checkpoint ROOT ID CAPTURE OBSERVATION_COMMAND HYPOTHESIS NEXT_COMMAND | checkpoint ROOT ID INTENT_FILE | materialize ROOT ANNOTATIONS (annotations: {evidence: [{capture, command, contract, expected, classification}], limits: [..]}; revision, runtime, cwd and learning are filled in)';
export async function qaEvidenceMain(args: string[]): Promise<number> {
return withQaReceiptOutput(false, 'qa-evidence-receipt', value => value.event === 'observation'
? JSON.stringify(value.observed) + '\n' : value.event === 'diagnostic' ? String(value.stderr)
: '\nQA_EVIDENCE ' + JSON.stringify({ producer: 'gstack-qa-evidence', version: 1, ...value }) + '\n', async emit => {
try {
const [action, reportRoot, ...rest] = args;
if (action === '--help' && args.length === 1) {
emit('stdout', { action: 'help', status: 'complete', usage: QA_EVIDENCE_USAGE, exitCode: 0 });
return 0;
}
const root = qaEvidenceRoot(reportRoot);
let receipt: Record<string, any>;
const publicOutput = action === 'capture' && rest[1] === '--public';
@@ -297,7 +303,7 @@ export async function qaEvidenceMain(args: string[]): Promise<number> {
} else if (action === 'checkpoint' && rest.length === 2) receipt = checkpoint(root, rest[0], rest[1]);
else if (action === 'checkpoint' && rest.length === 5) receipt = checkpoint(root, rest[0], { capture: rest[1], observationCommand: rest[2], hypothesis: rest[3], nextCommand: rest[4] });
else if (action === 'materialize' && rest.length === 1) receipt = materialize(root, rest[0]);
else throw new QaEvidenceError('Usage: capture ROOT ID [--public] --deadline FILE|--timeout-ms MS -- COMMAND ARGS | checkpoint ROOT ID CAPTURE OBSERVATION_COMMAND HYPOTHESIS NEXT_COMMAND | checkpoint ROOT ID INTENT_FILE | materialize ROOT ANNOTATIONS');
else throw new QaEvidenceError(`Usage: ${QA_EVIDENCE_USAGE}`);
emit('stdout', receipt);
return receipt.status === 'complete' ? receipt.exitCode : receipt.status === 'incomplete' ? receipt.exitCode || 2 : 2;
} catch (error) {