/** * check-task-infra.ts — report edits to toolkit-managed files in a task. * * Called by `scripts/harbor-run` before a trial and by `scripts/submit-task.ts` * before packaging, so an accidental edit to the trial Dockerfile, the grader * orchestration, or the grader system prompt surfaces at the moment it matters * rather than after a submission is reviewed. * * Exits 1 when a managed file was edited, 0 otherwise (including when this * toolkit ships no baselines to compare against). * * Usage: * npx tsx scripts/check-task-infra.ts * npx tsx scripts/check-task-infra.ts my-task --json */ import { existsSync } from 'fs'; import { basename, isAbsolute, join, resolve } from 'path'; import pino from 'pino'; import pinoPretty from 'pino-pretty'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { bannerize, checkTaskInfraIntegrity, formatIntegrityReport, writeManagedStamp, } from './lib/task-infra-integrity.js'; const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') .positional('task', { type: 'string', describe: 'Task slug, or a path to harbor-tasks/' }) .option('json', { type: 'boolean', describe: 'Output structured JSON logs', default: false, }) .option('stamp', { type: 'boolean', default: false, describe: 'Record the managed files as created, so later edits are detectable. No-op if already stamped.', }) .demandCommand(1, 'Provide a task slug or directory') .help() .parseSync(); const log = pino( { name: 'check-task-infra', level: 'info' }, argv.json ? process.stdout : pinoPretty({ colorize: true, translateTime: 'HH:MM:ss', ignore: 'pid,hostname' }) ); const arg = String(argv._[0]); const toolkitRoot = process.cwd(); // Accept both a bare slug and a path, since harbor-run is invoked with a path // (`scripts/harbor-run harbor-tasks/`) and submit-task with a slug. const taskDir = isAbsolute(arg) ? arg : existsSync(resolve(toolkitRoot, arg)) ? resolve(toolkitRoot, arg) : join(toolkitRoot, 'harbor-tasks', arg); if (!existsSync(taskDir)) { log.fatal({ taskDir }, 'Task directory not found'); process.exit(1); } const slug = basename(taskDir); // --stamp records a task's baseline. Runs at task creation; never overwrites. if (argv.stamp) { if (!existsSync(join(toolkitRoot, 'task-shared'))) { log.debug('Not a worker toolkit (no task-shared/); nothing to stamp'); process.exit(0); } const wrote = writeManagedStamp(taskDir, toolkitRoot); log.debug({ slug, wrote }, wrote ? 'Stamped toolkit-managed files' : 'Already stamped'); process.exit(0); } const report = checkTaskInfraIntegrity(taskDir, toolkitRoot); if (!report.checked) { log.debug('Not a worker toolkit (no task-shared/); skipping managed-file check'); process.exit(0); } const message = formatIntegrityReport(report); if (!message) { log.info({ files: report.files.length }, 'Toolkit-managed files are unmodified'); process.exit(0); } // Advisory, always. Exiting non-zero here is what used to let a false positive stop // an author's trial with no way out; the report is the whole product. log.warn( { edited: report.modified.map((f) => f.taskPath), outdated: report.outdated.map((f) => f.taskPath), unverifiable: report.unverifiable.map((f) => f.taskPath), }, 'Toolkit-managed files need a look' ); process.stderr.write(`\n${bannerize(message, report)}\n\n`); process.exit(0);