2026-07-04 22:53:10 +00:00
#!/usr/bin/env node
feat: content-pipeline/ → agents/ — formalize the agent system in the seed
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
2026-07-11 20:33:48 +00:00
/ * *
* @ agent - manifest
* name : seo - review
* title : SEO Reviewer
* class : content
* trigger : manual < slug > ( auto - invoked by writer / approve gates )
* description : Audit one article for EEAT / spam - policy / on - page SEO / AEO / readability → seo - review . json .
* model : reviewer = claude - opus - 4 - 8
* prompt : prompts / seo - review . system . md
* skills : -
* tools : lib / claude . mjs
* reads : -
* writes : -
* created : 2026 - 07 - 04
* @ end
* /
2026-07-04 22:53:10 +00:00
// SEO Specialist agent: audit one article for EEAT / spam-policy / on-page SEO / AEO / readability.
// Writes <postDir>/seo-review.json. Usage:
// node scripts/seo-review.mjs <slug> [--published]
feat: content-pipeline/ → agents/ — formalize the agent system in the seed
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
2026-07-11 20:33:48 +00:00
// (default: review the draft in agents/drafts/<slug>; --published reviews the
2026-07-04 22:53:10 +00:00
// live post in app/src/content/blog/<slug>)
import { existsSync , readFileSync } from "node:fs" ;
import { dirname , resolve , join } from "node:path" ;
import { fileURLToPath } from "node:url" ;
import { runClaude } from "./lib/claude.mjs" ;
import { loadJson , readFrontmatter , fmString } from "./lib/util.mjs" ;
import { readSlugs } from "./lib/blogData.mjs" ;
const HERE = dirname ( fileURLToPath ( import . meta . url ) ) ;
const PIPELINE = resolve ( HERE , ".." ) ;
const cfg = loadJson ( join ( PIPELINE , "config.json" ) ) ;
const root = cfg . paths . projectRoot ;
function postDirFor ( slug , published ) {
const draftDir = join ( root , cfg . paths . draftsDir , slug ) ;
const liveDir = join ( root , cfg . paths . blogContentDir , slug ) ;
if ( published ) return liveDir ;
if ( existsSync ( join ( draftDir , "index.mdx" ) ) ) return draftDir ;
if ( existsSync ( join ( liveDir , "index.mdx" ) ) ) return liveDir ;
return draftDir ; // will fail the existence check below
}
function primaryKeywordFor ( slug , mdxPath ) {
const calPath = join ( root , cfg . paths . calendar ) ;
if ( existsSync ( calPath ) ) {
const entry = loadJson ( calPath ) . find ( ( e ) => e . slug === slug ) ;
if ( entry ? . primaryKeyword ) return entry . primaryKeyword ;
}
if ( existsSync ( mdxPath ) ) {
const t = fmString ( readFrontmatter ( readFileSync ( mdxPath , "utf8" ) ) , "title" ) ;
if ( t ) return t ;
}
return slug . replace ( /-/g , " " ) ;
}
/** Run the SEO audit for one post. Returns the parsed audit object, or null on failure. */
export async function runSeoReview ( slug , { published = false } = { } ) {
const postDir = postDirFor ( slug , published ) ;
const mdxPath = join ( postDir , "index.mdx" ) ;
if ( ! existsSync ( mdxPath ) ) {
console . error ( ` [seo] no article at ${ mdxPath } ` ) ;
return null ;
}
const keyword = primaryKeywordFor ( slug , mdxPath ) ;
const calPathForEntry = join ( root , cfg . paths . calendar ) ;
const calEntry = existsSync ( calPathForEntry )
? loadJson ( calPathForEntry ) . find ( ( e ) => e . slug === slug )
: null ;
const firsthandNote = calEntry ? . authorFirsthand
? ` \n \n IMPORTANT — authorFirsthand piece: a real, named author ( ${ cfg . author ? . name || "the site author" } ) wrote this personally from genuine first-hand experience. First-person voice and subjective opinions are LEGITIMATE Experience (positive E-E-A-T), NOT fabrication. Do not flag the personal voice as an EEAT/spam violation; only require citations for objective, verifiable facts (addresses, prices, dates, hours). `
: "" ;
const blogDir = join ( root , cfg . paths . blogContentDir ) ;
const categories = [ ... readSlugs ( join ( root , cfg . paths . blogDataFile ) ) . categories ] ;
const auditPath = join ( postDir , "seo-review.json" ) ;
const logFile = join ( root , cfg . paths . logsDir , ` seo- ${ slug } .log ` ) ;
const prompt = ` Audit this Comiida article.
- Article : $ { mdxPath }
- Citations file ( if present ) : $ { join ( postDir , "sources.json" ) }
- Primary keyword to target : $ { keyword }
- Existing published posts ( for internal - link validation ) : $ { blogDir }
- Allowed category slugs : $ { categories . join ( ", " ) }
- Write your audit JSON to : $ { auditPath } $ { firsthandNote }
Follow the rubric and output contract in your system prompt exactly . ` ;
await runClaude ( {
prompt ,
systemPromptFile : join ( PIPELINE , "prompts/seo-review.system.md" ) ,
model : cfg . models . reviewer ,
allowedTools : "Read Glob Grep WebSearch Write" ,
addDirs : [ root ] ,
cwd : root ,
logFile ,
} ) ;
if ( ! existsSync ( auditPath ) ) {
console . error ( ` [seo] audit not written: ${ auditPath } ` ) ;
return null ;
}
try {
return loadJson ( auditPath ) ;
} catch ( e ) {
console . error ( ` [seo] could not parse audit: ${ e . message } ` ) ;
return null ;
}
}
// CLI
if ( import . meta . url === ` file:// ${ process . argv [ 1 ] } ` ) {
const slug = process . argv [ 2 ] ;
const published = process . argv . includes ( "--published" ) ;
if ( ! slug ) {
console . error ( "Usage: seo-review.mjs <slug> [--published]" ) ;
process . exit ( 1 ) ;
}
const audit = await runSeoReview ( slug , { published } ) ;
if ( ! audit ) process . exit ( 1 ) ;
const nb = ( audit . blocking || [ ] ) . length ;
const nf = ( audit . topFixes || [ ] ) . length ;
console . log ( ` [seo] ${ slug } : ${ audit . verdict } · ${ audit . overall } · ${ nb } blocking · ${ nf } fixes ` ) ;
if ( audit . summary ) console . log ( ` [seo] ${ audit . summary } ` ) ;
if ( nb ) console . log ( "[seo] blocking:\n - " + audit . blocking . join ( "\n - " ) ) ;
process . exit ( audit . verdict === "pass" ? 0 : audit . verdict === "revise" ? 2 : 3 ) ;
}