#!/usr/bin/env -S bun --no-env-file --config=/dev/null /** * gstack-artifact — validate, print and acknowledge the unattended artifact contract. * * gstack-artifact validate [--json] completion check for a run directory * gstack-artifact validate --public [--private ] [--json] * a public run copy: provenance, recorded machine roots * gstack-artifact validate --as [--run ] [--json] * one artifact against its schema * gstack-artifact schema | --list JSON Schema this version validates * gstack-artifact ack --consumer [--json] idempotent consumption report (P7) * gstack-artifact urgent the `## URGENT, outside this plan` block * * Query command: no GSTACK_RESULT line. `--json` means JSON only on stdout and * diagnostics on stderr. Logic: lib/headless-artifacts.ts; codes: lib/result-codes.ts. */ import * as path from 'node:path'; import { ARTIFACT_NAMES, EXIT, ackRun, exitTableHelp, jsonSchema, readJsonl, renderUrgentBlock, validateFile, validateRun, type ArtifactName, type ValidationError, } from '../lib/headless-artifacts'; import { resultLine } from '../lib/result-codes'; import { resolveStateRoot } from '../lib/state-root'; import { validatePublic } from '../lib/autoplan-public'; const HELP = `gstack-artifact — the unattended artifact contract (docs/unattended.md) validate [--json] run-directory completion check validate --public [--private ] a public run copy (gstack-autoplan export --public): provenance digests; with --private (on the exporting machine) any recorded root fails (ARTIFACT_MACHINE_PATH, file:line); home-path patterns WARN validate --as [--run ] [--json] one artifact against its schema schema | --list JSON Schema for ${ARTIFACT_NAMES.join(', ')} ack --consumer [--json] set run.json.consumed_by (idempotent) and log the ack urgent render the URGENT block (\`None.\` when empty) Without --json: one error line per problem on stderr, each ending in (), then ARTIFACT_VALID: run= status=[ attested= when voices bound on attestation] or ARTIFACT_INVALID: error(s) on stdout. ${exitTableHelp()}`; function flag(args: string[], name: string): string | undefined { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; } function usage(message: string): number { process.stderr.write(`gstack-artifact: ${message}\n${HELP}\n`); return EXIT.usage; } function printErrors(errors: ValidationError[]): void { for (const e of errors) process.stderr.write(resultLine('gstack-artifact validate', e.code, `${e.message} at ${e.path}`) + '\n'); } function main(argv: string[]): number { const [cmd, ...rest] = argv; const json = rest.includes('--json'); if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; } if (cmd === 'schema') { if (rest.includes('--list')) { process.stdout.write(ARTIFACT_NAMES.join('\n') + '\n'); return EXIT.ok; } const name = rest[0]; if (!name || !ARTIFACT_NAMES.includes(name as ArtifactName)) return usage(`schema needs one of: ${ARTIFACT_NAMES.join(', ')}`); process.stdout.write(JSON.stringify(jsonSchema(name as ArtifactName), null, 2) + '\n'); return EXIT.ok; } if (cmd === 'validate' && flag(rest, '--public') !== undefined) { const dir = flag(rest, '--public')!; const result = validatePublic(dir, flag(rest, '--private')); if (json) process.stdout.write(JSON.stringify({ valid: result.valid, run: result.run ?? null, public: dir, errors: result.errors, warnings: result.warnings }) + '\n'); else { printErrors(result.errors); for (const w of result.warnings) process.stderr.write(w + '\n'); process.stdout.write(result.valid ? `ARTIFACT_VALID: public run=${result.run} warnings=${result.warnings.length}${flag(rest, '--private') ? '' : ' (recorded roots unchecked: pass --private on the exporting machine)'}\n` : `ARTIFACT_INVALID: ${result.errors.length} error(s)\n`); } return result.valid ? EXIT.ok : EXIT.fail; } if (cmd === 'validate') { const file = rest.find(a => !a.startsWith('--') && a !== flag(rest, '--as') && a !== flag(rest, '--run')); if (!file) return usage('validate needs a file'); const as = flag(rest, '--as'); if (as !== undefined && !ARTIFACT_NAMES.includes(as as ArtifactName)) return usage(`--as needs one of: ${ARTIFACT_NAMES.join(', ')}`); if (as) { const errors = validateFile(file, as as ArtifactName, flag(rest, '--run')); if (json) process.stdout.write(JSON.stringify({ valid: errors.length === 0, file, as, errors }) + '\n'); else { printErrors(errors); process.stdout.write(errors.length ? `ARTIFACT_INVALID: ${errors.length} error(s)\n` : `ARTIFACT_VALID: ${as} ${path.basename(file)}\n`); } return errors.length ? EXIT.fail : EXIT.ok; } const result = validateRun(file); if (json) { process.stdout.write(JSON.stringify({ valid: result.valid, run: result.manifest?.run ?? null, status: result.manifest?.status ?? null, run_dir: result.runDir, attested: result.attested ?? 0, errors: result.errors }) + '\n'); } else { printErrors(result.errors); process.stdout.write(result.valid ? `ARTIFACT_VALID: run=${result.manifest!.run} status=${result.manifest!.status}${result.attested ? ` attested=${result.attested}` : ''}\n` : `ARTIFACT_INVALID: ${result.errors.length} error(s)\n`); } return result.valid ? EXIT.ok : EXIT.fail; } if (cmd === 'ack') { const file = rest.find(a => !a.startsWith('--') && a !== flag(rest, '--consumer')); const consumer = flag(rest, '--consumer'); if (!file || !consumer) return usage('ack needs --consumer '); const validation = validateRun(file); if (!validation.valid) { printErrors(validation.errors); process.stderr.write('gstack-artifact ack: refusing to acknowledge an invalid run; nothing written\n'); return EXIT.refused; } const analyticsPath = path.join(resolveStateRoot(), 'analytics', 'autoplan-timing.jsonl'); const result = ackRun(file, consumer, { analyticsPath, ephemeral: process.env.GSTACK_EPHEMERAL === '1' }); if (result.analytics === 'skipped_ephemeral') process.stderr.write('ack: analytics skipped (state root is ephemeral; set GSTACK_STATE_ROOT)\n'); if (json) process.stdout.write(JSON.stringify({ run: validation.manifest!.run, consumer, ...result }) + '\n'); else process.stdout.write(`ARTIFACT_ACK: run=${validation.manifest!.run} consumer=${consumer} ${result.added ? 'recorded' : 'already recorded'} analytics=${result.analytics}\n`); return EXIT.ok; } if (cmd === 'urgent') { const file = rest[0]; if (!file) return usage('urgent needs '); const read = readJsonl(file); if (read.errors.length) { printErrors(read.errors); return EXIT.fail; } process.stdout.write(renderUrgentBlock(read.rows)); return EXIT.ok; } return usage(`unknown command ${cmd}`); } process.exitCode = main(process.argv.slice(2));