diff options
| -rw-r--r-- | .claude/workflows/validate.js | 60 |
1 files changed, 57 insertions, 3 deletions
diff --git a/.claude/workflows/validate.js b/.claude/workflows/validate.js index 522548ee6..dadf8a77f 100644 --- a/.claude/workflows/validate.js +++ b/.claude/workflows/validate.js @@ -1,11 +1,11 @@ export const meta = { name: 'validate', - description: 'Pre-PR software validation: unit tests + per-board build sweeps + code-size compare + PVS, in parallel, joined into one verdict', + description: 'Pre-PR software validation: unit tests + per-board build sweeps + code-size compare + PVS + diff reviews (claude + codex), in parallel, joined into one verdict', whenToUse: 'Before opening or updating a PR, after any non-trivial change', - phases: [{ title: 'Validate', detail: 'unit + builds + size + pvs in parallel' }], + phases: [{ title: 'Validate', detail: 'unit + builds + size + pvs + reviews in parallel' }], } -// args: { boards: string[], examples?: string, base?: string, skip?: ('unit'|'size'|'pvs')[] } +// args: { boards: string[], examples?: string, base?: string, skip?: ('unit'|'size'|'pvs'|'review'|'codex')[] } if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } } if (!args || !Array.isArray(args.boards) || args.boards.length === 0) { throw new Error('args must be { boards: string[], examples?, base?, skip? }') @@ -56,6 +56,26 @@ const PVS = { }, } +const REVIEW = { + type: 'object', additionalProperties: false, + required: ['pass', 'findings', 'detail'], + properties: { + pass: { type: 'boolean' }, + findings: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['file', 'line', 'severity', 'summary'], + properties: { + file: { type: 'string' }, line: { type: 'integer' }, + severity: { type: 'string' }, summary: { type: 'string' }, + }, + }, + }, + detail: { type: 'string' }, + }, +} + const thunks = [] if (!skip.includes('unit')) thunks.push(() => @@ -92,6 +112,40 @@ if (!skip.includes('pvs')) thunks.push(() => detail: r.pass ? r.detail : clip(`${r.detail} ${JSON.stringify(r.changedFindings)}`), })) +if (!skip.includes('review')) thunks.push(() => + agent( + `Code-review this branch's diff vs ${base} (git diff ${base}...HEAD), coverage-first: walk every hunk, no spot checks. ` + + 'Find pass — candidate defects across all dimensions: correctness/logic, ISR & concurrency safety, ' + + 'memory/resource handling (bounds, leaks, no dynamic alloc), API contract & spec conformance, ' + + 'security of untrusted input parsing, behavior regressions; plus quality/simplification notes. ' + + 'Verify pass — adversarially check each candidate against the surrounding code: verdict CONFIRMED ' + + '(failing scenario constructed) or PLAUSIBLE (could not refute); report both, drop only refuted ones. ' + + 'Read-only: never apply fixes. severity = verdict plus category (e.g. "CONFIRMED correctness"). ' + + 'pass=false if any CONFIRMED correctness/safety/security bug survives; PLAUSIBLE and quality findings keep pass=true. ' + + 'detail = one-line review summary.', + { label: 'review', phase: 'Validate', model: 'opus', effort: 'high', schema: REVIEW }, + ).then(r => r && { + stage: 'review', + // gate enforced here, not trusted from the agent: any CONFIRMED non-quality finding fails + pass: r.pass && !r.findings.some(f => + /^confirmed/i.test(f.severity) && !/quality|simplification|style/i.test(f.severity)), + findings: r.findings, detail: r.detail, + })) + +if (!skip.includes('codex')) thunks.push(() => + agent( + `Run a Codex review of this branch's diff vs ${base}: ` + + `codex review --base ${base} -c model="gpt-5.6-sol" -c model_reasoning_effort="high" ` + + '(Bash timeout 600000; run from the repo root). Parse its output into findings; severity = Codex\'s priority label. ' + + 'pass=false only if Codex reports a correctness bug (P0/P1); style-level items keep pass=true. ' + + 'detail = Codex\'s overall verdict line. If the codex CLI is missing or the run errors, pass=false with the error in detail.', + { label: 'codex', phase: 'Validate', model: 'haiku', schema: REVIEW }, + ).then(r => r && { + stage: 'codex', + pass: r.pass && !r.findings.some(f => /\bP[01]\b/i.test(f.severity)), + findings: r.findings, detail: r.detail, + })) + const results = (await parallel(thunks)).filter(Boolean) const dead = thunks.length - results.length if (dead > 0) log(`${dead} stage agent(s) died — counted as failures`) |
