#!/usr/bin/env node /** * @agent-manifest * name: reviser * title: Draft Reviser * class: content * trigger: manual (auto-invoked when the SEO gate fails and autoRevise is on) * description: Feed the SEO audit's fixes back to the writer and re-audit until the draft passes or attempts run out. * model: writer=claude-sonnet-4-6 * prompt: prompts/reviser.system.md * skills: - * tools: lib/claude.mjs * reads: - * writes: - * created: 2026-07-04 * @end */ // Auto-revise loop: feed the SEO audit's fixes back to the writer, re-audit, repeat until the // draft passes the gate or maxReviseAttempts is reached. // Usage: node agents/scripts/revise.mjs import { existsSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; import { runClaude } from "./lib/claude.mjs"; import { loadJson } from "./lib/util.mjs"; import { runSeoReview } from "./seo-review.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const PIPELINE = resolve(HERE, ".."); const cfg = loadJson(join(PIPELINE, "config.json")); const root = cfg.paths.projectRoot; const failing = (audit, minScore) => !audit || audit.verdict === "fail" || (audit.overall ?? 0) < minScore; /** * Revise a draft until it passes the SEO gate (or attempts run out). * @returns {Promise<{audit:object|null, attempts:number}>} */ export async function runRevise(slug, { maxAttempts } = {}) { const max = maxAttempts ?? cfg.seo?.maxReviseAttempts ?? 2; const minScore = cfg.seo?.minScore ?? 85; const draftDir = join(root, cfg.paths.draftsDir, slug); const mdxPath = join(draftDir, "index.mdx"); const auditPath = join(draftDir, "seo-review.json"); if (!existsSync(mdxPath)) { console.error(`[revise] no draft at ${mdxPath}`); return { audit: null, attempts: 0 }; } const calPathForEntry = join(root, cfg.paths.calendar); const calEntry = existsSync(calPathForEntry) ? loadJson(calPathForEntry).find((e) => e.slug === slug) : null; const firsthandNote = calEntry?.authorFirsthand ? `\n\nIMPORTANT — authorFirsthand piece: a real, named author (${cfg.author?.name || "the site author"}) wrote this personally. Do NOT strip or neutralize the first-person voice and subjective opinions — that is authentic Experience and must be kept. Only add citations for objective facts (addresses, prices, dates, hours) or frame them honestly.` : ""; let audit = existsSync(auditPath) ? loadJson(auditPath) : await runSeoReview(slug, { published: false }); let attempts = 0; while (failing(audit, minScore) && attempts < max) { attempts++; const keyword = audit?.primaryKeyword || slug.replace(/-/g, " "); console.log(`[revise] attempt ${attempts}/${max} on ${slug} (current: ${audit?.verdict} ${audit?.overall})`); const prompt = `Revise this draft to pass the SEO/EEAT gate (target ≥ ${minScore}). - Draft: ${mdxPath} - Citations file: ${join(draftDir, "sources.json")} - Audit to address (work through blocking + topFixes): ${auditPath} - Primary keyword: ${keyword} - Existing published posts (for internal links): ${join(root, cfg.paths.blogContentDir)}${firsthandNote} Follow the reviser instructions in your system prompt exactly. Edit the files in place.`; await runClaude({ prompt, systemPromptFile: join(PIPELINE, "prompts/reviser.system.md"), model: cfg.models.writer, allowedTools: "Read Write Edit Glob Grep WebSearch", addDirs: [root], cwd: root, logFile: join(root, cfg.paths.logsDir, `revise-${slug}.log`), }); audit = await runSeoReview(slug, { published: false }); } const ok = !failing(audit, minScore); console.log( `[revise] ${slug}: ${ok ? "PASS" : "still failing"} after ${attempts} attempt(s) — ` + `${audit?.verdict} ${audit?.overall}` ); return { audit, attempts }; } // CLI if (import.meta.url === `file://${process.argv[1]}`) { const slug = process.argv[2]; if (!slug) { console.error("Usage: revise.mjs "); process.exit(1); } const { audit } = await runRevise(slug); process.exit(audit && audit.verdict === "pass" ? 0 : 2); }