← back to Codex Yolo
scripts/apply-trivial-patches.cjs
126 lines
#!/usr/bin/env node
// apply-trivial-patches.cjs
// Reads a finished claude-codex debate dir → spawns Claude Code CLI with a
// strict prompt → asks for ONLY high-confidence trivial fixes as a JSON list
// of {file, search, replace, kind} → applies them.
// LOGIC changes get diff-printed to <project>/_yolo_patches/iter-N.diff for
// Steve's morning review — never auto-applied.
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const args = Object.fromEntries(
process.argv.slice(2).reduce((acc, a, i, A) => {
if (a.startsWith('--')) acc.push([a.slice(2), A[i+1]]);
return acc;
}, [])
);
const DEBATE = args.debate;
const PROJECT = args.project ? path.resolve(args.project) : args.project;
const ITER = args.iter || '1';
const MAX_APP = parseInt(args['max-applied'] || '5', 10);
if (!DEBATE || !PROJECT) {
console.error('usage: apply-trivial-patches.cjs --debate DIR --project DIR [--iter N] [--max-applied N]');
process.exit(2);
}
const finalReport = (() => {
const fp = path.join(DEBATE, 'final_report.md');
if (fs.existsSync(fp)) return fs.readFileSync(fp, 'utf8');
// Fallback: concat round_*/*.txt
const rounds = fs.readdirSync(DEBATE).filter(n => /^round_\d+$/.test(n));
let buf = '';
for (const r of rounds) {
for (const f of fs.readdirSync(path.join(DEBATE, r))) {
buf += `\n=== ${r}/${f} ===\n` + fs.readFileSync(path.join(DEBATE, r, f), 'utf8');
}
}
return buf;
})();
if (!finalReport.trim()) {
console.log('[apply] empty debate report, nothing to do');
process.exit(0);
}
const patchDir = path.join(PROJECT, '_yolo_patches');
fs.mkdirSync(patchDir, { recursive: true });
// Strict prompt — asks Claude Code CLI for a JSON-only response of trivial patches
const prompt = `You are reviewing a claude-codex 8-way debate report for a code project.
Project root: ${PROJECT}
# Debate report (truncated)
${finalReport.slice(0, 18000)}
# Your job
Output ONLY a single JSON object on one line. No prose, no markdown fence.
The shape:
{ "trivial": [ {"file":"<rel path>","search":"<EXACT substring>","replace":"<new substring>","kind":"typo|null-guard|missing-await|regex-escape|off-by-one"} ], "logic": [ {"file":"<rel>","summary":"<1-line>","unified_diff":"<full diff>"} ] }
Rules:
- "trivial" entries MUST satisfy ALL: ≤1 file, ≤10 lines changed, ≤120-char search string, search appears exactly once in the file, replacement preserves identifiers used elsewhere.
- Skip anything that touches: dw_unified, Shopify, DNS, payment, auth, schema migration, deletes data.
- If unsure, demote to "logic" with a unified_diff for review.
- If the debate report is empty / inconclusive, output {"trivial":[],"logic":[]}.
- Cap "trivial" at ${MAX_APP}.
`;
let response;
try {
response = execFileSync('/Users/macstudio3/.claude/local/claude', [
'-p', prompt,
'--output-format','text',
'--allowed-tools','Read,Glob,Grep'
], { encoding: 'utf8', maxBuffer: 4*1024*1024, timeout: 6*60*1000 });
} catch {
// Fallback path: many machines have `claude` on PATH instead
try {
response = execFileSync('claude', [
'-p', prompt, '--output-format','text',
'--allowed-tools','Read,Glob,Grep'
], { encoding:'utf8', maxBuffer: 4*1024*1024, timeout: 6*60*1000 });
} catch (e2) {
console.error('[apply] claude CLI failed:', e2.message);
process.exit(0);
}
}
// Pull JSON out of the response (may have trailing whitespace / a fence)
const m = response.match(/\{[\s\S]*\}/);
if (!m) { console.log('[apply] no JSON in claude response'); process.exit(0); }
let data;
try { data = JSON.parse(m[0]); }
catch (e) { console.log('[apply] JSON parse fail:', e.message); process.exit(0); }
const trivial = Array.isArray(data.trivial) ? data.trivial.slice(0, MAX_APP) : [];
const logic = Array.isArray(data.logic) ? data.logic : [];
// Write logic-change diff for Steve's morning review (NEVER apply)
if (logic.length) {
const diff = logic.map(l => `# ${l.file}\n# ${l.summary}\n${l.unified_diff || '(no diff supplied)'}`).join('\n\n');
fs.writeFileSync(path.join(patchDir, `iter-${ITER}.diff`), diff);
console.log(`[apply] wrote ${logic.length} proposed logic patches → _yolo_patches/iter-${ITER}.diff`);
}
// Apply trivial patches one by one with strict safety
let applied = 0;
for (const p of trivial) {
try {
const fp = path.resolve(PROJECT, p.file);
if (fp !== PROJECT && !fp.startsWith(PROJECT + path.sep)) { console.log('[apply] path escape, skip:', p.file); continue; }
if (!fs.existsSync(fp)) { console.log('[apply] missing:', p.file); continue; }
const before = fs.readFileSync(fp, 'utf8');
const occ = before.split(p.search).length - 1;
if (occ !== 1) { console.log(`[apply] search not unique (${occ}) in ${p.file}, skip`); continue; }
if (Math.abs(p.replace.length - p.search.length) > 600) { console.log('[apply] huge replace, skip'); continue; }
fs.writeFileSync(fp, before.replace(p.search, p.replace));
console.log(`[apply] ${p.kind}: ${p.file}`);
applied++;
} catch (e) {
console.log('[apply] one patch failed:', e.message);
}
}
console.log(`[apply] applied=${applied} proposed_logic=${logic.length}`);