diff --git a/.gitignore b/.gitignore index a854b7d..3db0404 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # ============================================================================ # SeedProject base — cloneable Astro + PHP foundation -# Tracked: engine code (app/src, api framework, content-pipeline, astroagent). +# Tracked: engine code (app/src, api framework, agents, astroagent). # Ignored: secrets, dependencies, build output, and per-site runtime state. # ============================================================================ @@ -25,17 +25,17 @@ app/.astro/ *.log # --- per-site runtime state (regenerated; dirs kept via .gitkeep) --- -content-pipeline/logs/* -!content-pipeline/logs/.gitkeep -content-pipeline/drafts/* -!content-pipeline/drafts/.gitkeep -content-pipeline/state/* -!content-pipeline/state/.gitkeep -content-pipeline/calendar.json -content-pipeline/messages.json -content-pipeline/news-queue.json -content-pipeline/news-scan.json -content-pipeline/research-scan.json +agents/logs/* +!agents/logs/.gitkeep +agents/drafts/* +!agents/drafts/.gitkeep +agents/state/* +!agents/state/.gitkeep +agents/calendar.json +agents/messages.json +agents/news-queue.json +agents/news-scan.json +agents/research-scan.json # --- astroagent runtime workspaces --- .astroagent/jobs/ diff --git a/AGENTS.md b/AGENTS.md index 21b0af7..6d37377 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ name, URL, description, tagline, author, social, topic, audience, timezone. configs. - To change identity: edit `site.config.json`, then run `node scripts/configure.mjs`. That regenerates `app/src/config/site.json` (the theme reads it) and updates - `content-pipeline/config.json` and `astroagent.config.json`. + `agents/config.json` and `astroagent.config.json`. - In the Astro theme, read identity from `SITE`, `authors`, etc. exported by `app/src/lib/blog-data.js` (which imports `app/src/config/site.json`). @@ -48,7 +48,7 @@ name, URL, description, tagline, author, social, topic, audience, timezone. | `scripts/configure.mjs` | Stamp `site.config.json` into every engine | | `app/` | Astro frontend ("theme"). Build → `../public`. See `app/AGENTS.md` for coding standards. | | `api/` | SeedProject PHP backend, served at `/api` (see below) | -| `content-pipeline/` | Autonomous content engine (research → write → review → publish) | +| `agents/` | LLM agents: orchestrators + prompts + config. Catalog: `agents/AGENTS.md` (generated) | | `public/` | Build output (git-ignored) | ## Backend (`api/`) @@ -72,11 +72,15 @@ php console db:migrate --status ## Content pipeline & authoring -- `content-pipeline/` runs the autonomous content system. `configure.mjs` stamps the - site name/topic/audience/author into `content-pipeline/config.json`, and its runtime - state (`drafts/`, `state/`, `logs/`) is git-ignored and regenerates per site. +- `agents/` runs the autonomous content system. Each agent script carries an + `@agent-manifest` header (class, trigger, model, prompts, skills, tools, tables); + `node agents/catalog.mjs` regenerates the catalog (`agents/AGENTS.md` + + `agents/agents.json` — the machine manifest a clone can diff to find missing deps). + `configure.mjs` stamps the site name/topic/audience/author into `agents/config.json` + and regenerates the catalog; runtime state (`drafts/`, `state/`, `logs/`) is + git-ignored and regenerates per site. - **⚠️ NOT yet niche-generic — do not trust the pipeline output as-is.** The prompts and - some scripts (`content-pipeline/prompts/*.system.md`, `content-pipeline/scripts/*.mjs`) + some scripts (`agents/prompts/*.system.md`, `agents/scripts/*.mjs`) still contain wording from the original site (a Medellín restaurant publication) and do **not** read the niche from config. Running the pipeline before genericizing it will produce off-niche content. Fix: make those files read `config.site.{name,topic,audience}`. diff --git a/README.md b/README.md index a09ea27..17dc513 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ WordPress install — but faster to host and safer to run. |------|------|------------------------| | `app/` | Astro theme — layouts, components, content collections, sample post | theme + posts | | `api/` | [SeedProject](api/.memory/documentation.md) PHP framework — the dynamic backend (DB, forms, metrics, agents) | PHP core | -| `content-pipeline/` | Autonomous content engine — research, write, review, publish | (no equivalent) | +| `agents/` | Autonomous content engine — research, write, review, publish | (no equivalent) | | `astroagent.config.json` + `app/.astroagent/` | AI authoring console — change the site in plain English | wp-admin | | `site.config.json` | **Single source of site identity** — the one file you edit per site | wp-config + Site Settings | @@ -55,7 +55,7 @@ node scripts/configure.mjs ``` This stamps the values into the Astro theme (`app/src/config/site.json`), -`content-pipeline/config.json`, and `astroagent.config.json` so all three engines +`agents/config.json`, and `astroagent.config.json` so all three engines share one identity. `new-site.sh` runs it for you. ## Backend (`api/`) @@ -78,8 +78,8 @@ The `api/` directory is the SeedProject PHP framework, meant to be served at ## Content pipeline & authoring -- **`content-pipeline/`** — autonomous research/write/review/publish scripts driven - by `content-pipeline/config.json` (populated from `site.config.json`). Runtime +- **`agents/`** — autonomous research/write/review/publish scripts driven + by `agents/config.json` (populated from `site.config.json`). Runtime state (`drafts/`, `state/`, `logs/`, queues) is git-ignored and regenerates per site. - **astroagent** — the in-site console for making changes in plain English; its identity comes from `astroagent.config.json`. @@ -94,7 +94,7 @@ my-site/ │ └── configure.mjs ← stamp site.config.json into every engine ├── app/ ← Astro frontend (build → ../public) ├── api/ ← SeedProject PHP backend (served at /api) -├── content-pipeline/ ← autonomous content engine +├── agents/ ← autonomous content engine └── public/ ← build output (git-ignored) ``` diff --git a/agents/AGENTS.md b/agents/AGENTS.md new file mode 100644 index 0000000..42179d4 --- /dev/null +++ b/agents/AGENTS.md @@ -0,0 +1,32 @@ +# Agent catalog + +> **GENERATED — do not edit.** Regenerate with `node agents/catalog.mjs` +> (source of truth: the `@agent-manifest` header in each script). + +An **agent** = an LLM orchestrator script + its role prompt (`prompt:`) + its skills +(MCP servers, `skills:`) + its tools (`api/cli/*.php` bridges and `lib/*.mjs`, `tools:`). +To port an agent to another SeedProject clone, satisfy its manifest: copy its prompts, ensure +its tools exist, migrate each table in `reads:`/`writes:`, configure its MCP servers. +`agents.json` is the machine-readable version — diff it against a clone to find gaps. + +## content + +Cron/manual-triggered producers of gated drafts (events auto-publishes). + +| agent | trigger | model | skills | tools | reads → writes | description | +|---|---|---|---|---|---|---| +| **news-radar** ([agents/scripts/news-radar.mjs](../agents/scripts/news-radar.mjs)) | manual / cron-capable (maintains news-queue.json for the writer) | research=claude-opus-4-8 | — | lib/claude.mjs | — → — | Discover timely niche news and maintain news-queue.json; the daily writer drains it first. | +| **research** ([agents/scripts/research.mjs](../agents/scripts/research.mjs)) | manual (auto-invoked by writer when the evergreen backlog runs low) | research=claude-opus-4-8 | — | lib/claude.mjs, lib/calendar.mjs | — → — | Top up the evergreen backlog in calendar.json (additive, deduped); timely news is the news radar's job. | +| **reviser** ([agents/scripts/revise.mjs](../agents/scripts/revise.mjs)) | manual (auto-invoked when the SEO gate fails and autoRevise is on) | writer=claude-sonnet-4-6 | — | lib/claude.mjs | — → — | Feed the SEO audit's fixes back to the writer and re-audit until the draft passes or attempts run out. | +| **seo-review** ([agents/scripts/seo-review.mjs](../agents/scripts/seo-review.mjs)) | manual (auto-invoked by writer/approve gates) | reviewer=claude-opus-4-8 | — | lib/claude.mjs | — → — | Audit one article for EEAT / spam-policy / on-page SEO / AEO / readability → seo-review.json. | +| **writer** ([agents/scripts/write-daily.mjs](../agents/scripts/write-daily.mjs)) | cron:run.sh write-daily.mjs(daily) | writer=claude-sonnet-4-6 | mcp__claude_ai_Higgsfield | lib/claude.mjs, lib/calendar.mjs | — → — | Draft one article (MDX + cover + sources) — today's news first, else next evergreen calendar topic. | + +## plumbing + +Deterministic helpers — not agents (no LLM), catalogued for completeness. + +| agent | trigger | model | skills | tools | reads → writes | description | +|---|---|---|---|---|---|---| +| **approve** ([agents/scripts/approve.mjs](../agents/scripts/approve.mjs)) | manual [--force] | - | — | lib/publish.mjs | — → — | Approve a draft: SEO gate, then promote it into the blog, build, go live. No LLM. | +| **list-drafts** ([agents/scripts/list-drafts.mjs](../agents/scripts/list-drafts.mjs)) | manual | - | — | — | — → — | List pending drafts awaiting review. No LLM. | +| **publish-tick** ([agents/scripts/publish-tick.mjs](../agents/scripts/publish-tick.mjs)) | cron:run.sh publish-tick.mjs(15m) | - | — | lib/publish.mjs | — → — | Once per day after the morning floor, publish ONE eligible draft (SEO-gated) with a randomized earlier-today timestamp. No LLM. | diff --git a/content-pipeline/README.md b/agents/README.md similarity index 100% rename from content-pipeline/README.md rename to agents/README.md diff --git a/content-pipeline/admin/server.mjs b/agents/admin/server.mjs similarity index 99% rename from content-pipeline/admin/server.mjs rename to agents/admin/server.mjs index 312372a..29a2698 100644 --- a/content-pipeline/admin/server.mjs +++ b/agents/admin/server.mjs @@ -93,7 +93,7 @@ const badge = (status) => const newsBadge = `NEWS`; const suggestedBadge = `SUGGESTED`; function freshNewsCount() { - const p = join(root, "content-pipeline", "news-queue.json"); + const p = join(root, "agents", "news-queue.json"); if (!existsSync(p)) return 0; try { return JSON.parse(readFileSync(p, "utf8")).filter((i) => i.status === "fresh").length; @@ -108,7 +108,7 @@ const seoBadge = (seo) => ? `SEO ${esc(seo.verdict)} ${esc(seo.overall)}` : `SEO —`; -const messagesPath = () => join(root, "content-pipeline", "messages.json"); +const messagesPath = () => join(root, "agents", "messages.json"); function messages() { const p = messagesPath(); if (!existsSync(p)) return []; diff --git a/agents/agents.json b/agents/agents.json new file mode 100644 index 0000000..4875a4f --- /dev/null +++ b/agents/agents.json @@ -0,0 +1,153 @@ +{ + "agents": [ + { + "file": "agents/scripts/approve.mjs", + "name": "approve", + "title": "Draft Approver", + "class": "plumbing", + "trigger": "manual [--force]", + "description": "Approve a draft: SEO gate, then promote it into the blog, build, go live. No LLM.", + "model": "-", + "prompt": [], + "skills": [], + "tools": [ + "lib/publish.mjs" + ], + "reads": [], + "writes": [], + "created": "2026-07-04" + }, + { + "file": "agents/scripts/list-drafts.mjs", + "name": "list-drafts", + "title": "Draft Lister", + "class": "plumbing", + "trigger": "manual", + "description": "List pending drafts awaiting review. No LLM.", + "model": "-", + "prompt": [], + "skills": [], + "tools": [], + "reads": [], + "writes": [], + "created": "2026-07-04" + }, + { + "file": "agents/scripts/news-radar.mjs", + "name": "news-radar", + "title": "News Radar", + "class": "content", + "trigger": "manual / cron-capable (maintains news-queue.json for the writer)", + "description": "Discover timely niche news and maintain news-queue.json; the daily writer drains it first.", + "model": "research=claude-opus-4-8", + "prompt": [ + "prompts/news-radar.system.md" + ], + "skills": [], + "tools": [ + "lib/claude.mjs" + ], + "reads": [], + "writes": [], + "created": "2026-07-04" + }, + { + "file": "agents/scripts/publish-tick.mjs", + "name": "publish-tick", + "title": "Auto-Publisher Tick", + "class": "plumbing", + "trigger": "cron:run.sh publish-tick.mjs(15m)", + "description": "Once per day after the morning floor, publish ONE eligible draft (SEO-gated) with a randomized earlier-today timestamp. No LLM.", + "model": "-", + "prompt": [], + "skills": [], + "tools": [ + "lib/publish.mjs" + ], + "reads": [], + "writes": [], + "created": "2026-07-04" + }, + { + "file": "agents/scripts/research.mjs", + "name": "research", + "title": "Editorial Calendar Researcher", + "class": "content", + "trigger": "manual (auto-invoked by writer when the evergreen backlog runs low)", + "description": "Top up the evergreen backlog in calendar.json (additive, deduped); timely news is the news radar's job.", + "model": "research=claude-opus-4-8", + "prompt": [ + "prompts/research.system.md" + ], + "skills": [], + "tools": [ + "lib/claude.mjs", + "lib/calendar.mjs" + ], + "reads": [], + "writes": [], + "created": "2026-07-04" + }, + { + "file": "agents/scripts/revise.mjs", + "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" + }, + { + "file": "agents/scripts/seo-review.mjs", + "name": "seo-review", + "title": "SEO Reviewer", + "class": "content", + "trigger": "manual (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" + }, + { + "file": "agents/scripts/write-daily.mjs", + "name": "writer", + "title": "Daily Blog Writer", + "class": "content", + "trigger": "cron:run.sh write-daily.mjs(daily)", + "description": "Draft one article (MDX + cover + sources) — today's news first, else next evergreen calendar topic.", + "model": "writer=claude-sonnet-4-6", + "prompt": [ + "prompts/writer.system.md", + "prompts/image.md" + ], + "skills": [ + "mcp__claude_ai_Higgsfield" + ], + "tools": [ + "lib/claude.mjs", + "lib/calendar.mjs" + ], + "reads": [], + "writes": [], + "created": "2026-07-04" + } + ] +} diff --git a/agents/catalog.mjs b/agents/catalog.mjs new file mode 100644 index 0000000..fe2f01b --- /dev/null +++ b/agents/catalog.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +// catalog.mjs — generate the agent catalog from @agent-manifest headers. +// +// Scans agents/scripts/**/*.mjs plus the runtime PHP agents listed below, extracts each +// fenced `@agent-manifest … @end` block, and emits: +// agents/AGENTS.md — human catalog, one table per class (GENERATED — do not edit) +// agents/agents.json — machine manifest; clones diff this to find missing deps +// +// Run after adding/editing an agent: node agents/catalog.mjs +// (also runs automatically at the end of scripts/configure.mjs) +import { readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, ".."); + +// Runtime agents living outside agents/ (PHP, served over HTTP) — included explicitly. +const RUNTIME_FILES = []; // no runtime PHP agents in the seed base yet + +const CLASSES = ["content", "operational", "runtime", "plumbing"]; +const LIST_KEYS = ["prompt", "skills", "tools", "reads", "writes"]; + +function parseManifest(src) { + const m = src.match(/@agent-manifest\s*\n([\s\S]*?)\n\s*\*?\s*@end/); + if (!m) return null; + const out = {}; + for (const raw of m[1].split("\n")) { + const line = raw.replace(/^\s*\*\s?/, ""); // strip comment leader + const kv = line.match(/^([a-z-]+):\s*(.*)$/); + if (kv) out[kv[1]] = kv[2].trim(); + } + for (const k of LIST_KEYS) { + const v = out[k]; + out[k] = !v || v === "-" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean); + } + return out; +} + +const files = readdirSync(join(HERE, "scripts")) + .filter((f) => f.endsWith(".mjs")) + .map((f) => join("agents", "scripts", f)) + .concat(RUNTIME_FILES); + +const agents = []; +for (const rel of files.sort()) { + const manifest = parseManifest(readFileSync(join(ROOT, rel), "utf8")); + if (!manifest) continue; // no manifest → not an agent (e.g. future helpers) + if (!manifest.name || !manifest.class) { + console.error(`[catalog] ${rel}: manifest missing name/class — skipped`); + continue; + } + if (!CLASSES.includes(manifest.class)) { + console.error(`[catalog] ${rel}: unknown class "${manifest.class}" — skipped`); + continue; + } + agents.push({ file: rel, ...manifest }); +} +agents.sort((a, b) => a.name.localeCompare(b.name)); + +// ---- agents.json (machine manifest) ---- +writeFileSync(join(HERE, "agents.json"), JSON.stringify({ agents }, null, 2) + "\n"); + +// ---- AGENTS.md (human catalog) ---- +const esc = (s) => String(s).replace(/\|/g, "\\|"); +const classBlurb = { + content: "Cron/manual-triggered producers of gated drafts (events auto-publishes).", + operational: "Event/queue-triggered reviewers and responders (gated + logged).", + runtime: "HTTP-triggered agents answering live user requests.", + plumbing: "Deterministic helpers — not agents (no LLM), catalogued for completeness.", +}; + +let md = `# Agent catalog + +> **GENERATED — do not edit.** Regenerate with \`node agents/catalog.mjs\` +> (source of truth: the \`@agent-manifest\` header in each script). + +An **agent** = an LLM orchestrator script + its role prompt (\`prompt:\`) + its skills +(MCP servers, \`skills:\`) + its tools (\`api/cli/*.php\` bridges and \`lib/*.mjs\`, \`tools:\`). +To port an agent to another SeedProject clone, satisfy its manifest: copy its prompts, ensure +its tools exist, migrate each table in \`reads:\`/\`writes:\`, configure its MCP servers. +\`agents.json\` is the machine-readable version — diff it against a clone to find gaps. +`; + +for (const cls of CLASSES) { + const group = agents.filter((a) => a.class === cls); + if (!group.length) continue; + md += `\n## ${cls}\n\n${classBlurb[cls]}\n\n`; + md += `| agent | trigger | model | skills | tools | reads → writes | description |\n`; + md += `|---|---|---|---|---|---|---|\n`; + for (const a of group) { + const rw = `${a.reads.join(", ") || "—"} → ${a.writes.join(", ") || "—"}`; + md += `| **${esc(a.name)}** ([${esc(a.file)}](../${a.file})) | ${esc(a.trigger || "—")} | ${esc(a.model || "—")} | ${esc(a.skills.join(", ") || "—")} | ${esc(a.tools.join(", ") || "—")} | ${esc(rw)} | ${esc(a.description || "")} |\n`; + } +} + +writeFileSync(join(HERE, "AGENTS.md"), md); +console.log(`[catalog] ${agents.length} manifests → agents/AGENTS.md + agents/agents.json`); diff --git a/content-pipeline/config.json b/agents/config.json similarity index 91% rename from content-pipeline/config.json rename to agents/config.json index 1fc6d5e..c272ddd 100644 --- a/content-pipeline/config.json +++ b/agents/config.json @@ -11,9 +11,11 @@ "appDir": "app", "blogContentDir": "app/src/content/blog", "blogDataFile": "app/src/lib/blog-data.js", - "calendar": "content-pipeline/calendar.json", - "draftsDir": "content-pipeline/drafts", - "logsDir": "content-pipeline/logs" + "calendar": "agents/calendar.json", + "draftsDir": "agents/drafts", + "logsDir": "agents/logs", + "stateDir": "agents/state", + "newsDir": "agents" }, "models": { "research": "claude-opus-4-8", diff --git a/content-pipeline/drafts/.gitkeep b/agents/drafts/.gitkeep similarity index 100% rename from content-pipeline/drafts/.gitkeep rename to agents/drafts/.gitkeep diff --git a/content-pipeline/logs/.gitkeep b/agents/logs/.gitkeep similarity index 100% rename from content-pipeline/logs/.gitkeep rename to agents/logs/.gitkeep diff --git a/content-pipeline/prompts/image.md b/agents/prompts/image.md similarity index 100% rename from content-pipeline/prompts/image.md rename to agents/prompts/image.md diff --git a/content-pipeline/prompts/news-radar.system.md b/agents/prompts/news-radar.system.md similarity index 100% rename from content-pipeline/prompts/news-radar.system.md rename to agents/prompts/news-radar.system.md diff --git a/content-pipeline/prompts/research.system.md b/agents/prompts/research.system.md similarity index 100% rename from content-pipeline/prompts/research.system.md rename to agents/prompts/research.system.md diff --git a/content-pipeline/prompts/reviser.system.md b/agents/prompts/reviser.system.md similarity index 100% rename from content-pipeline/prompts/reviser.system.md rename to agents/prompts/reviser.system.md diff --git a/content-pipeline/prompts/seo-review.system.md b/agents/prompts/seo-review.system.md similarity index 100% rename from content-pipeline/prompts/seo-review.system.md rename to agents/prompts/seo-review.system.md diff --git a/content-pipeline/prompts/writer.system.md b/agents/prompts/writer.system.md similarity index 98% rename from content-pipeline/prompts/writer.system.md rename to agents/prompts/writer.system.md index 1c60320..56f42eb 100644 --- a/content-pipeline/prompts/writer.system.md +++ b/agents/prompts/writer.system.md @@ -54,7 +54,7 @@ output is a reviewable draft — it does not go live until a human approves it. the primary keyword. ## Output — write exactly these files into the draft directory -The user prompt gives you the draft dir as `content-pipeline/drafts//`. +The user prompt gives you the draft dir as `agents/drafts//`. **1. `index.mdx`** — frontmatter must match the Astro schema EXACTLY, then the body: ```mdx diff --git a/content-pipeline/run.sh b/agents/run.sh similarity index 100% rename from content-pipeline/run.sh rename to agents/run.sh diff --git a/content-pipeline/scripts/approve.mjs b/agents/scripts/approve.mjs similarity index 83% rename from content-pipeline/scripts/approve.mjs rename to agents/scripts/approve.mjs index 26942eb..1497bad 100644 --- a/content-pipeline/scripts/approve.mjs +++ b/agents/scripts/approve.mjs @@ -1,6 +1,22 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: approve + * title: Draft Approver + * class: plumbing + * trigger: manual [--force] + * description: Approve a draft: SEO gate, then promote it into the blog, build, go live. No LLM. + * model: - + * prompt: - + * skills: - + * tools: lib/publish.mjs + * reads: - + * writes: - + * created: 2026-07-04 + * @end + */ // Manually approve a draft: SEO gate, then promote it into the blog, build, go live. -// Usage: node content-pipeline/scripts/approve.mjs [--force] +// Usage: node agents/scripts/approve.mjs [--force] import { existsSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/content-pipeline/scripts/lib/blogData.mjs b/agents/scripts/lib/blogData.mjs similarity index 100% rename from content-pipeline/scripts/lib/blogData.mjs rename to agents/scripts/lib/blogData.mjs diff --git a/content-pipeline/scripts/lib/build.mjs b/agents/scripts/lib/build.mjs similarity index 90% rename from content-pipeline/scripts/lib/build.mjs rename to agents/scripts/lib/build.mjs index 48e54d3..440433a 100644 --- a/content-pipeline/scripts/lib/build.mjs +++ b/agents/scripts/lib/build.mjs @@ -15,7 +15,7 @@ function run(cmd, args, opts = {}) { /** Absolute path of the global build lock. Callers that build outside buildSite() * (e.g. console preview builds) must take THIS same lock to serialize. */ export function buildLockPath(projectRoot) { - return `${projectRoot}/content-pipeline/state/build.lock`; + return `${projectRoot}/agents/state/build.lock`; } /** @@ -25,7 +25,7 @@ export function buildLockPath(projectRoot) { */ export async function buildSite({ projectRoot, appDir }) { const lockPath = buildLockPath(projectRoot); - mkdirSync(`${projectRoot}/content-pipeline/state`, { recursive: true }); + mkdirSync(`${projectRoot}/agents/state`, { recursive: true }); await withLock(lockPath, async () => { await run("npm", ["run", "build"], { cwd: `${projectRoot}/${appDir}` }); // Best-effort ownership fix (ignore failure if not running as root). diff --git a/content-pipeline/scripts/lib/calendar.mjs b/agents/scripts/lib/calendar.mjs similarity index 100% rename from content-pipeline/scripts/lib/calendar.mjs rename to agents/scripts/lib/calendar.mjs diff --git a/content-pipeline/scripts/lib/claude.mjs b/agents/scripts/lib/claude.mjs similarity index 100% rename from content-pipeline/scripts/lib/claude.mjs rename to agents/scripts/lib/claude.mjs diff --git a/content-pipeline/scripts/lib/git.mjs b/agents/scripts/lib/git.mjs similarity index 100% rename from content-pipeline/scripts/lib/git.mjs rename to agents/scripts/lib/git.mjs diff --git a/content-pipeline/scripts/lib/lock.mjs b/agents/scripts/lib/lock.mjs similarity index 100% rename from content-pipeline/scripts/lib/lock.mjs rename to agents/scripts/lib/lock.mjs diff --git a/content-pipeline/scripts/lib/publish.mjs b/agents/scripts/lib/publish.mjs similarity index 100% rename from content-pipeline/scripts/lib/publish.mjs rename to agents/scripts/lib/publish.mjs diff --git a/content-pipeline/scripts/lib/util.mjs b/agents/scripts/lib/util.mjs similarity index 100% rename from content-pipeline/scripts/lib/util.mjs rename to agents/scripts/lib/util.mjs diff --git a/content-pipeline/scripts/list-drafts.mjs b/agents/scripts/list-drafts.mjs similarity index 74% rename from content-pipeline/scripts/list-drafts.mjs rename to agents/scripts/list-drafts.mjs index 3452066..3a50d3e 100644 --- a/content-pipeline/scripts/list-drafts.mjs +++ b/agents/scripts/list-drafts.mjs @@ -1,6 +1,22 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: list-drafts + * title: Draft Lister + * class: plumbing + * trigger: manual + * description: List pending drafts awaiting review. No LLM. + * model: - + * prompt: - + * skills: - + * tools: - + * reads: - + * writes: - + * created: 2026-07-04 + * @end + */ // List pending drafts awaiting review. -// Usage: node content-pipeline/scripts/list-drafts.mjs +// Usage: node agents/scripts/list-drafts.mjs import { readdirSync, existsSync, readFileSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -34,5 +50,5 @@ for (const d of dirs) { } console.log(` • ${d.name}`); console.log(` ${title}${cat ? ` [${cat}]` : ""} cover:${hasCover ? "yes" : "NO"}`); - console.log(` approve: node content-pipeline/scripts/approve.mjs ${d.name}\n`); + console.log(` approve: node agents/scripts/approve.mjs ${d.name}\n`); } diff --git a/content-pipeline/scripts/news-radar.mjs b/agents/scripts/news-radar.mjs similarity index 86% rename from content-pipeline/scripts/news-radar.mjs rename to agents/scripts/news-radar.mjs index 78c96c2..885be0b 100644 --- a/content-pipeline/scripts/news-radar.mjs +++ b/agents/scripts/news-radar.mjs @@ -1,6 +1,22 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: news-radar + * title: News Radar + * class: content + * trigger: manual / cron-capable (maintains news-queue.json for the writer) + * description: Discover timely niche news and maintain news-queue.json; the daily writer drains it first. + * model: research=claude-opus-4-8 + * prompt: prompts/news-radar.system.md + * skills: - + * tools: lib/claude.mjs + * reads: - + * writes: - + * created: 2026-07-04 + * @end + */ // News radar: discover timely Medellín food news/events and maintain news-queue.json. -// Usage: node content-pipeline/scripts/news-radar.mjs +// Usage: node agents/scripts/news-radar.mjs import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -12,8 +28,8 @@ const PIPELINE = resolve(HERE, ".."); const cfg = loadJson(join(PIPELINE, "config.json")); const root = cfg.paths.projectRoot; -const queuePath = join(root, "content-pipeline", "news-queue.json"); -const scanPath = join(root, "content-pipeline", "news-scan.json"); +const queuePath = join(root, cfg.paths.newsDir, "news-queue.json"); +const scanPath = join(root, cfg.paths.newsDir, "news-scan.json"); const loadArr = (p) => (existsSync(p) ? loadJson(p) : []); const publishedSlugs = () => { diff --git a/content-pipeline/scripts/publish-tick.mjs b/agents/scripts/publish-tick.mjs similarity index 87% rename from content-pipeline/scripts/publish-tick.mjs rename to agents/scripts/publish-tick.mjs index 520dbdb..4d460b6 100644 --- a/content-pipeline/scripts/publish-tick.mjs +++ b/agents/scripts/publish-tick.mjs @@ -1,9 +1,25 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: publish-tick + * title: Auto-Publisher Tick + * class: plumbing + * trigger: cron:run.sh publish-tick.mjs(15m) + * description: Once per day after the morning floor, publish ONE eligible draft (SEO-gated) with a randomized earlier-today timestamp. No LLM. + * model: - + * prompt: - + * skills: - + * tools: lib/publish.mjs + * reads: - + * writes: - + * created: 2026-07-04 + * @end + */ // Auto-publisher. Run frequently by cron (e.g. every 15 min). Once per day, after a morning // floor time, it publishes ONE eligible draft (passing the SEO gate) so posts go live in the // morning — but stamps each with a RANDOM earlier-today timestamp so published times aren't a // fixed-minute metronome. News drafts are fast-tracked ahead of evergreen. -// Usage: node content-pipeline/scripts/publish-tick.mjs [--now] (--now ignores the floor) +// Usage: node agents/scripts/publish-tick.mjs [--now] (--now ignores the floor) import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -22,7 +38,7 @@ if (!cfg.publish?.auto) { } const today = todayInTz(cfg.editorial.timezone); -const stateDir = join(root, "content-pipeline", "state"); +const stateDir = join(root, cfg.paths.stateDir); mkdirSync(stateDir, { recursive: true }); const statePath = join(stateDir, `publish-${today}.json`); diff --git a/content-pipeline/scripts/research.mjs b/agents/scripts/research.mjs similarity index 84% rename from content-pipeline/scripts/research.mjs rename to agents/scripts/research.mjs index 6464b2e..3f2a06f 100644 --- a/content-pipeline/scripts/research.mjs +++ b/agents/scripts/research.mjs @@ -1,7 +1,23 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: research + * title: Editorial Calendar Researcher + * class: content + * trigger: manual (auto-invoked by writer when the evergreen backlog runs low) + * description: Top up the evergreen backlog in calendar.json (additive, deduped); timely news is the news radar's job. + * model: research=claude-opus-4-8 + * prompt: prompts/research.system.md + * skills: - + * tools: lib/claude.mjs, lib/calendar.mjs + * reads: - + * writes: - + * created: 2026-07-04 + * @end + */ // Editorial research: top up the EVERGREEN backlog in calendar.json (additive, deduped). // Timely news is handled separately by the news radar. Usage: -// node content-pipeline/scripts/research.mjs [count] +// node agents/scripts/research.mjs [count] import { readdirSync, existsSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -26,7 +42,7 @@ export async function runResearch({ count } = {}) { const n = count || cfg.editorial.calendarDays; const today = todayInTz(cfg.editorial.timezone); const calendarPath = join(root, cfg.paths.calendar); - const scanPath = join(root, "content-pipeline", "research-scan.json"); + const scanPath = join(root, "agents", "research-scan.json"); // Per-type target counts from the configured mix. const types = Object.keys(cfg.editorial.contentMix); diff --git a/content-pipeline/scripts/revise.mjs b/agents/scripts/revise.mjs similarity index 86% rename from content-pipeline/scripts/revise.mjs rename to agents/scripts/revise.mjs index a5fa1cb..13af100 100644 --- a/content-pipeline/scripts/revise.mjs +++ b/agents/scripts/revise.mjs @@ -1,7 +1,23 @@ #!/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 content-pipeline/scripts/revise.mjs +// Usage: node agents/scripts/revise.mjs import { existsSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/content-pipeline/scripts/seo-review.mjs b/agents/scripts/seo-review.mjs similarity index 88% rename from content-pipeline/scripts/seo-review.mjs rename to agents/scripts/seo-review.mjs index e33e964..53f9504 100644 --- a/content-pipeline/scripts/seo-review.mjs +++ b/agents/scripts/seo-review.mjs @@ -1,8 +1,24 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: seo-review + * title: SEO Reviewer + * class: content + * trigger: manual (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 + */ // SEO Specialist agent: audit one article for EEAT / spam-policy / on-page SEO / AEO / readability. // Writes /seo-review.json. Usage: // node scripts/seo-review.mjs [--published] -// (default: review the draft in content-pipeline/drafts/; --published reviews the +// (default: review the draft in agents/drafts/; --published reviews the // live post in app/src/content/blog/) import { existsSync, readFileSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; diff --git a/content-pipeline/scripts/write-daily.mjs b/agents/scripts/write-daily.mjs similarity index 92% rename from content-pipeline/scripts/write-daily.mjs rename to agents/scripts/write-daily.mjs index 7fe153d..d6cc0ee 100644 --- a/content-pipeline/scripts/write-daily.mjs +++ b/agents/scripts/write-daily.mjs @@ -1,8 +1,24 @@ #!/usr/bin/env node +/** + * @agent-manifest + * name: writer + * title: Daily Blog Writer + * class: content + * trigger: cron:run.sh write-daily.mjs(daily) + * description: Draft one article (MDX + cover + sources) — today's news first, else next evergreen calendar topic. + * model: writer=claude-sonnet-4-6 + * prompt: prompts/writer.system.md, prompts/image.md + * skills: mcp__claude_ai_Higgsfield + * tools: lib/claude.mjs, lib/calendar.mjs + * reads: - + * writes: - + * created: 2026-07-04 + * @end + */ // Daily writer: draft one article (MDX + cover.jpg + sources.json) into drafts//. // Selection order: today's news (news radar) first → else the next evergreen calendar topic. // Refills the evergreen backlog on demand when it runs low. All calendar writes are atomic. -// Usage: node content-pipeline/scripts/write-daily.mjs [slug] +// Usage: node agents/scripts/write-daily.mjs [slug] import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -26,7 +42,7 @@ if (cfg.author.slug === "REPLACE_ME") { const calendarPath = join(root, cfg.paths.calendar); const today = todayInTz(cfg.editorial.timezone); -const stateDir = join(root, "content-pipeline", "state"); +const stateDir = join(root, cfg.paths.stateDir); const argSlug = process.argv[2]; let entry; @@ -128,7 +144,7 @@ ${JSON.stringify({ ...entry, draft: undefined, instructions: undefined }, null, - Draft output directory (write index.mdx, cover.jpg, sources.json here): ${draftDir} - Existing published posts to read for internal links: ${blogDir} - Image: use the Higgsfield MCP (preferred model "${cfg.image.preferredModel}", aspect ratio - ~${cfg.image.aspectRatio}). Read content-pipeline/prompts/image.md and follow that flow + ~${cfg.image.aspectRatio}). Read agents/prompts/image.md and follow that flow (recommend model → generate_image → job_status sync → curl the result URL). Save the final image to: ${join(draftDir, "cover.jpg")}. ${ diff --git a/content-pipeline/state/.gitkeep b/agents/state/.gitkeep similarity index 100% rename from content-pipeline/state/.gitkeep rename to agents/state/.gitkeep diff --git a/astroagent.config.json b/astroagent.config.json index 9f46827..1608beb 100644 --- a/astroagent.config.json +++ b/astroagent.config.json @@ -23,7 +23,7 @@ "console": { "port": 3011, "route": "/devconsole", - "tokenFile": "content-pipeline/.env", + "tokenFile": "agents/.env", "tokenKey": "ADMIN_TOKEN" } } diff --git a/scripts/configure.mjs b/scripts/configure.mjs index 2245c51..38fb4f1 100755 --- a/scripts/configure.mjs +++ b/scripts/configure.mjs @@ -7,8 +7,10 @@ * * Writes: * - app/src/config/site.json (Astro theme reads this) - * - content-pipeline/config.json (site identity + author + timezone) + * - agents/config.json (site identity + author + timezone) * - astroagent.config.json (name + url) + * + * Finishes by regenerating the agent catalog (agents/AGENTS.md + agents.json). */ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, resolve } from "node:path"; @@ -38,15 +40,15 @@ write("app/src/config/site.json", { social: site.social, }); -// 2) content-pipeline -if (existsSync(resolve(root, "content-pipeline/config.json"))) { - const cp = read("content-pipeline/config.json"); +// 2) agents +if (existsSync(resolve(root, "agents/config.json"))) { + const cp = read("agents/config.json"); cp.site = { ...cp.site, url: site.url, name: site.name, topic: site.topic, audience: site.audience, language: site.language }; cp.author = { ...(cp.author || {}), slug: site.author.slug, name: site.author.name }; if (cp.paths) cp.paths.projectRoot = root; // this clone's absolute path, not comiida's if (cp.editorial) cp.editorial.timezone = site.timezone; if (cp.image && cp.image.credit) cp.image.credit.author = site.name; - write("content-pipeline/config.json", cp); + write("agents/config.json", cp); } // 3) astroagent @@ -58,4 +60,10 @@ if (existsSync(resolve(root, "astroagent.config.json"))) { write("astroagent.config.json", aa); } +// 4) agent catalog (AGENTS.md + agents.json reflect this clone's identity) +if (existsSync(resolve(root, "agents/catalog.mjs"))) { + const { execFileSync } = await import("node:child_process"); + execFileSync("node", [resolve(root, "agents/catalog.mjs")], { stdio: "inherit" }); +} + console.log("Done. Rebuild the site: (cd app && npm run build)"); diff --git a/scripts/new-site.sh b/scripts/new-site.sh index 9796d3e..14593db 100755 --- a/scripts/new-site.sh +++ b/scripts/new-site.sh @@ -6,7 +6,7 @@ # ./scripts/new-site.sh # # Prompts for this site's identity, writes site.config.json, stamps it across -# every engine (Astro theme, content-pipeline, astroagent), optionally resets +# every engine (Astro theme, agents, astroagent), optionally resets # git history and installs dependencies. # --------------------------------------------------------------------------- set -euo pipefail