diff --git a/docs/agent-workflows/codex-pr-checklist.md b/docs/agent-workflows/codex-pr-checklist.md index cc705102b..346a735d5 100644 --- a/docs/agent-workflows/codex-pr-checklist.md +++ b/docs/agent-workflows/codex-pr-checklist.md @@ -4,6 +4,14 @@ Use this checklist for Codex web sessions, Linear-launched implementation agents ## Required Preflight +Run the scriptable preflight wrapper (recommended): + +```bash +node scripts/codex-pr-preflight.mjs --strict-path --lightweight +``` + +Use `--lightweight` when you only need environment/repo checks plus changed-surface validation recommendations (it skips heavier runtime validations). + Run this before editing files: ```bash diff --git a/scripts/codex-pr-preflight.mjs b/scripts/codex-pr-preflight.mjs new file mode 100755 index 000000000..63580c8de --- /dev/null +++ b/scripts/codex-pr-preflight.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; + +const REQUIRED_FILES = ['AGENTS.md', 'docs/src/README.md', 'Cargo.toml', 'app/package.json']; +const APP_PATTERNS = [/^app\//, /^docs\//]; +const ROOT_RUST_PATTERNS = [/^src\//, /^tests\//, /^Cargo\.toml$/, /^Cargo\.lock$/]; +const TAURI_PATTERNS = [/^app\/src-tauri\//]; + +function hasPattern(files, patterns) { + return files.some((file) => patterns.some((pattern) => pattern.test(file))); +} + +function runGit(command, repoRoot) { + return execSync(command, { cwd: repoRoot, encoding: 'utf8' }).trim(); +} + +function parseArgs(argv) { + return { + lightweight: argv.includes('--lightweight'), + strictPath: argv.includes('--strict-path'), + expectedPath: process.env.CODEX_EXPECT_REPO_PATH || '/workspace/openhuman', + }; +} + +function runCheck(label, ok, details = '') { + return { label, ok, details }; +} + +function summarize(checks) { + const failed = checks.filter((check) => !check.ok); + for (const check of checks) { + const prefix = check.ok ? 'PASS' : 'FAIL'; + const details = check.details ? ` :: ${check.details}` : ''; + console.log(`[${prefix}] ${check.label}${details}`); + } + return failed.length; +} + +function recommendations(changedFiles, lightweight) { + const lines = []; + if (hasPattern(changedFiles, APP_PATTERNS)) { + lines.push('pnpm --filter openhuman-app format:check'); + lines.push('pnpm typecheck'); + lines.push('pnpm --dir app exec vitest run --config test/vitest.config.ts'); + } + if (hasPattern(changedFiles, ROOT_RUST_PATTERNS)) { + lines.push('cargo fmt --manifest-path Cargo.toml --all --check'); + if (!lightweight) lines.push('pnpm debug rust '); + } + if (hasPattern(changedFiles, TAURI_PATTERNS)) { + lines.push('cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check'); + } + return [...new Set(lines)]; +} + +function main() { + const options = parseArgs(process.argv.slice(2)); + const repoRoot = process.cwd(); + const checks = []; + + checks.push(runCheck('working directory exists', fs.existsSync(repoRoot), repoRoot)); + if (options.strictPath) { + checks.push(runCheck('expected repo path', path.resolve(repoRoot) === path.resolve(options.expectedPath), `expected ${options.expectedPath}, got ${repoRoot}`)); + } + + for (const file of REQUIRED_FILES) { + checks.push(runCheck(`required file: ${file}`, fs.existsSync(path.join(repoRoot, file)))); + } + + let branch = ''; + let remotes = ''; + let changed = ''; + try { + branch = runGit('git branch --show-current', repoRoot); + checks.push(runCheck('branch naming convention', /^codex\/[A-Z]+-\d+[-a-z0-9]*$/i.test(branch), branch)); + } catch (error) { + checks.push(runCheck('branch readable', false, String(error))); + } + + try { + remotes = runGit('git remote -v', repoRoot); + if (!remotes) { + checks.push(runCheck('git remote configured (recommended)', true, 'no remotes configured in this checkout')); + } else { + const hasExpectedRemote = /(jwalin-shah\/openhuman|tinyhumansai\/openhuman)/.test(remotes); + checks.push(runCheck('expected git remote present', hasExpectedRemote)); + } + } catch (error) { + checks.push(runCheck('git remote readable', false, String(error))); + } + + try { + changed = runGit('git diff --name-only --diff-filter=ACMR HEAD', repoRoot); + checks.push(runCheck('changed files readable', true, changed || 'no changed files')); + } catch (error) { + checks.push(runCheck('changed files readable', false, String(error))); + } + + const changedFiles = changed ? changed.split('\n').filter(Boolean) : []; + const commandRecommendations = recommendations(changedFiles, options.lightweight); + + console.log('\nRecommended validation commands:'); + if (commandRecommendations.length === 0) { + console.log('- (none) no changed files detected'); + } else { + for (const cmd of commandRecommendations) console.log(`- ${cmd}`); + } + + const failures = summarize(checks); + if (failures > 0) process.exit(1); +} + +main(); diff --git a/scripts/test-codex-pr-preflight.mjs b/scripts/test-codex-pr-preflight.mjs new file mode 100755 index 000000000..09ead97d4 --- /dev/null +++ b/scripts/test-codex-pr-preflight.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; + +function run(cmd, cwd) { + return execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf8' }); +} + +function makeRepo(branchName) { + const dir = mkdtempSync(path.join(tmpdir(), 'codex-preflight-')); + mkdirSync(path.join(dir, 'docs/src'), { recursive: true }); + mkdirSync(path.join(dir, 'app'), { recursive: true }); + writeFileSync(path.join(dir, 'AGENTS.md'), '# test\n'); + writeFileSync(path.join(dir, 'docs/src/README.md'), 'ok\n'); + writeFileSync(path.join(dir, 'Cargo.toml'), '[package]\nname="x"\nversion="0.1.0"\n'); + writeFileSync(path.join(dir, 'app/package.json'), '{"name":"x"}\n'); + run('git init', dir); + run('git config user.email test@example.com', dir); + run('git config user.name test', dir); + run('git add .', dir); + run('git commit -m init', dir); + run(`git checkout -b ${branchName}`, dir); + run('git remote add origin git@github.com:jwalin-shah/openhuman.git', dir); + return dir; +} + +const script = path.resolve('scripts/codex-pr-preflight.mjs'); +const passRepo = makeRepo('codex/SYM-93-preflight'); +run(`CODEX_EXPECT_REPO_PATH=${passRepo} node ${script} --strict-path --lightweight`, passRepo); + +const failRepo = makeRepo('feature/not-codex'); +let failed = false; +try { + run(`CODEX_EXPECT_REPO_PATH=${failRepo} node ${script} --strict-path --lightweight`, failRepo); +} catch { + failed = true; +} +if (!failed) { + throw new Error('Expected invalid branch naming to fail preflight'); +} + +console.log('codex preflight self-test passed');