Adopt the agents/ architecture proven on medellin.co (reference impl): - Move the content engine to a top-level agents/ dir: orchestrators, prompts, config, run.sh, admin console, shared libs. All content-pipeline literals repointed (config paths, scripts, admin, LLM-facing prompts/image.md string, configure.mjs, new-site.sh, astroagent tokenFile, .gitignore runtime block). - Every script carries a parseable @agent-manifest header: name, title, class (content|operational|runtime|plumbing), trigger, model, prompts, skills (MCP), tools, reads/writes tables. 5 content agents + 3 plumbing scripts. - New agents/catalog.mjs generates the catalog from the headers: agents/AGENTS.md (human, grouped by class) + agents/agents.json (machine manifest — a clone diffs it against a source to find missing tools/tables/MCP before running). configure.mjs regenerates the catalog on every identity stamp. No DB table, no watcher. - config.json gains paths.stateDir/newsDir; publish-tick, write-daily, and news-radar read them instead of hardcoding. - Full cut: content-pipeline/ deleted (the seed has no live crons, so no hybrid period needed). Docs updated (AGENTS.md structure + pipeline section, README paths). Clones migrating from content-pipeline/: see medellin.co's .memory/handoffs/agents-directory-migration.md for the cutover playbook (one cron set active at a time; migrate drafts/state after repointing cron). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G
107 lines
4.1 KiB
JavaScript
107 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @agent-manifest
|
|
* name: reviser
|
|
* title: Draft Reviser
|
|
* class: content
|
|
* trigger: manual <slug> (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 <slug>
|
|
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 <slug>");
|
|
process.exit(1);
|
|
}
|
|
const { audit } = await runRevise(slug);
|
|
process.exit(audit && audit.verdict === "pass" ? 0 : 2);
|
|
}
|