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
This commit is contained in:
parent
41f1b941fa
commit
2c969c0753
38 changed files with 478 additions and 53 deletions
24
.gitignore
vendored
24
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SeedProject base — cloneable Astro + PHP foundation
|
# 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.
|
# Ignored: secrets, dependencies, build output, and per-site runtime state.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
@ -25,17 +25,17 @@ app/.astro/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# --- per-site runtime state (regenerated; dirs kept via .gitkeep) ---
|
# --- per-site runtime state (regenerated; dirs kept via .gitkeep) ---
|
||||||
content-pipeline/logs/*
|
agents/logs/*
|
||||||
!content-pipeline/logs/.gitkeep
|
!agents/logs/.gitkeep
|
||||||
content-pipeline/drafts/*
|
agents/drafts/*
|
||||||
!content-pipeline/drafts/.gitkeep
|
!agents/drafts/.gitkeep
|
||||||
content-pipeline/state/*
|
agents/state/*
|
||||||
!content-pipeline/state/.gitkeep
|
!agents/state/.gitkeep
|
||||||
content-pipeline/calendar.json
|
agents/calendar.json
|
||||||
content-pipeline/messages.json
|
agents/messages.json
|
||||||
content-pipeline/news-queue.json
|
agents/news-queue.json
|
||||||
content-pipeline/news-scan.json
|
agents/news-scan.json
|
||||||
content-pipeline/research-scan.json
|
agents/research-scan.json
|
||||||
|
|
||||||
# --- astroagent runtime workspaces ---
|
# --- astroagent runtime workspaces ---
|
||||||
.astroagent/jobs/
|
.astroagent/jobs/
|
||||||
|
|
|
||||||
16
AGENTS.md
16
AGENTS.md
|
|
@ -35,7 +35,7 @@ name, URL, description, tagline, author, social, topic, audience, timezone.
|
||||||
configs.
|
configs.
|
||||||
- To change identity: edit `site.config.json`, then run `node scripts/configure.mjs`.
|
- 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
|
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
|
- 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`).
|
`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 |
|
| `scripts/configure.mjs` | Stamp `site.config.json` into every engine |
|
||||||
| `app/` | Astro frontend ("theme"). Build → `../public`. See `app/AGENTS.md` for coding standards. |
|
| `app/` | Astro frontend ("theme"). Build → `../public`. See `app/AGENTS.md` for coding standards. |
|
||||||
| `api/` | SeedProject PHP backend, served at `/api` (see below) |
|
| `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) |
|
| `public/` | Build output (git-ignored) |
|
||||||
|
|
||||||
## Backend (`api/`)
|
## Backend (`api/`)
|
||||||
|
|
@ -72,11 +72,15 @@ php console db:migrate --status
|
||||||
|
|
||||||
## Content pipeline & authoring
|
## Content pipeline & authoring
|
||||||
|
|
||||||
- `content-pipeline/` runs the autonomous content system. `configure.mjs` stamps the
|
- `agents/` runs the autonomous content system. Each agent script carries an
|
||||||
site name/topic/audience/author into `content-pipeline/config.json`, and its runtime
|
`@agent-manifest` header (class, trigger, model, prompts, skills, tools, tables);
|
||||||
state (`drafts/`, `state/`, `logs/`) is git-ignored and regenerates per site.
|
`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
|
- **⚠️ 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
|
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
|
**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}`.
|
produce off-niche content. Fix: make those files read `config.site.{name,topic,audience}`.
|
||||||
|
|
|
||||||
10
README.md
10
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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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`),
|
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.
|
share one identity. `new-site.sh` runs it for you.
|
||||||
|
|
||||||
## Backend (`api/`)
|
## Backend (`api/`)
|
||||||
|
|
@ -78,8 +78,8 @@ The `api/` directory is the SeedProject PHP framework, meant to be served at
|
||||||
|
|
||||||
## Content pipeline & authoring
|
## Content pipeline & authoring
|
||||||
|
|
||||||
- **`content-pipeline/`** — autonomous research/write/review/publish scripts driven
|
- **`agents/`** — autonomous research/write/review/publish scripts driven
|
||||||
by `content-pipeline/config.json` (populated from `site.config.json`). Runtime
|
by `agents/config.json` (populated from `site.config.json`). Runtime
|
||||||
state (`drafts/`, `state/`, `logs/`, queues) is git-ignored and regenerates per site.
|
state (`drafts/`, `state/`, `logs/`, queues) is git-ignored and regenerates per site.
|
||||||
- **astroagent** — the in-site console for making changes in plain English; its
|
- **astroagent** — the in-site console for making changes in plain English; its
|
||||||
identity comes from `astroagent.config.json`.
|
identity comes from `astroagent.config.json`.
|
||||||
|
|
@ -94,7 +94,7 @@ my-site/
|
||||||
│ └── configure.mjs ← stamp site.config.json into every engine
|
│ └── configure.mjs ← stamp site.config.json into every engine
|
||||||
├── app/ ← Astro frontend (build → ../public)
|
├── app/ ← Astro frontend (build → ../public)
|
||||||
├── api/ ← SeedProject PHP backend (served at /api)
|
├── api/ ← SeedProject PHP backend (served at /api)
|
||||||
├── content-pipeline/ ← autonomous content engine
|
├── agents/ ← autonomous content engine
|
||||||
└── public/ ← build output (git-ignored)
|
└── public/ ← build output (git-ignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
32
agents/AGENTS.md
Normal file
32
agents/AGENTS.md
Normal file
|
|
@ -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 <slug> (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 <slug> (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 <slug> [--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. |
|
||||||
|
|
@ -93,7 +93,7 @@ const badge = (status) =>
|
||||||
const newsBadge = `<span style="background:#b5179e;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">NEWS</span>`;
|
const newsBadge = `<span style="background:#b5179e;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">NEWS</span>`;
|
||||||
const suggestedBadge = `<span style="background:#7048e8;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">SUGGESTED</span>`;
|
const suggestedBadge = `<span style="background:#7048e8;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">SUGGESTED</span>`;
|
||||||
function freshNewsCount() {
|
function freshNewsCount() {
|
||||||
const p = join(root, "content-pipeline", "news-queue.json");
|
const p = join(root, "agents", "news-queue.json");
|
||||||
if (!existsSync(p)) return 0;
|
if (!existsSync(p)) return 0;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(readFileSync(p, "utf8")).filter((i) => i.status === "fresh").length;
|
return JSON.parse(readFileSync(p, "utf8")).filter((i) => i.status === "fresh").length;
|
||||||
|
|
@ -108,7 +108,7 @@ const seoBadge = (seo) =>
|
||||||
? `<span style="background:${SEO_COLORS[seo.verdict] || "#666"};color:#fff;border-radius:999px;padding:2px 10px;font-size:12px;white-space:nowrap">SEO ${esc(seo.verdict)} ${esc(seo.overall)}</span>`
|
? `<span style="background:${SEO_COLORS[seo.verdict] || "#666"};color:#fff;border-radius:999px;padding:2px 10px;font-size:12px;white-space:nowrap">SEO ${esc(seo.verdict)} ${esc(seo.overall)}</span>`
|
||||||
: `<span style="background:#aaa;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">SEO —</span>`;
|
: `<span style="background:#aaa;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">SEO —</span>`;
|
||||||
|
|
||||||
const messagesPath = () => join(root, "content-pipeline", "messages.json");
|
const messagesPath = () => join(root, "agents", "messages.json");
|
||||||
function messages() {
|
function messages() {
|
||||||
const p = messagesPath();
|
const p = messagesPath();
|
||||||
if (!existsSync(p)) return [];
|
if (!existsSync(p)) return [];
|
||||||
153
agents/agents.json
Normal file
153
agents/agents.json
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
{
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"file": "agents/scripts/approve.mjs",
|
||||||
|
"name": "approve",
|
||||||
|
"title": "Draft Approver",
|
||||||
|
"class": "plumbing",
|
||||||
|
"trigger": "manual <slug> [--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 <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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "agents/scripts/seo-review.mjs",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
98
agents/catalog.mjs
Normal file
98
agents/catalog.mjs
Normal file
|
|
@ -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`);
|
||||||
|
|
@ -11,9 +11,11 @@
|
||||||
"appDir": "app",
|
"appDir": "app",
|
||||||
"blogContentDir": "app/src/content/blog",
|
"blogContentDir": "app/src/content/blog",
|
||||||
"blogDataFile": "app/src/lib/blog-data.js",
|
"blogDataFile": "app/src/lib/blog-data.js",
|
||||||
"calendar": "content-pipeline/calendar.json",
|
"calendar": "agents/calendar.json",
|
||||||
"draftsDir": "content-pipeline/drafts",
|
"draftsDir": "agents/drafts",
|
||||||
"logsDir": "content-pipeline/logs"
|
"logsDir": "agents/logs",
|
||||||
|
"stateDir": "agents/state",
|
||||||
|
"newsDir": "agents"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"research": "claude-opus-4-8",
|
"research": "claude-opus-4-8",
|
||||||
|
|
@ -54,7 +54,7 @@ output is a reviewable draft — it does not go live until a human approves it.
|
||||||
the primary keyword.
|
the primary keyword.
|
||||||
|
|
||||||
## Output — write exactly these files into the draft directory
|
## Output — write exactly these files into the draft directory
|
||||||
The user prompt gives you the draft dir as `content-pipeline/drafts/<slug>/`.
|
The user prompt gives you the draft dir as `agents/drafts/<slug>/`.
|
||||||
|
|
||||||
**1. `index.mdx`** — frontmatter must match the Astro schema EXACTLY, then the body:
|
**1. `index.mdx`** — frontmatter must match the Astro schema EXACTLY, then the body:
|
||||||
```mdx
|
```mdx
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* @agent-manifest
|
||||||
|
* name: approve
|
||||||
|
* title: Draft Approver
|
||||||
|
* class: plumbing
|
||||||
|
* trigger: manual <slug> [--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.
|
// Manually approve a draft: SEO gate, then promote it into the blog, build, go live.
|
||||||
// Usage: node content-pipeline/scripts/approve.mjs <slug> [--force]
|
// Usage: node agents/scripts/approve.mjs <slug> [--force]
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -15,7 +15,7 @@ function run(cmd, args, opts = {}) {
|
||||||
/** Absolute path of the global build lock. Callers that build outside buildSite()
|
/** Absolute path of the global build lock. Callers that build outside buildSite()
|
||||||
* (e.g. console preview builds) must take THIS same lock to serialize. */
|
* (e.g. console preview builds) must take THIS same lock to serialize. */
|
||||||
export function buildLockPath(projectRoot) {
|
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 }) {
|
export async function buildSite({ projectRoot, appDir }) {
|
||||||
const lockPath = buildLockPath(projectRoot);
|
const lockPath = buildLockPath(projectRoot);
|
||||||
mkdirSync(`${projectRoot}/content-pipeline/state`, { recursive: true });
|
mkdirSync(`${projectRoot}/agents/state`, { recursive: true });
|
||||||
await withLock(lockPath, async () => {
|
await withLock(lockPath, async () => {
|
||||||
await run("npm", ["run", "build"], { cwd: `${projectRoot}/${appDir}` });
|
await run("npm", ["run", "build"], { cwd: `${projectRoot}/${appDir}` });
|
||||||
// Best-effort ownership fix (ignore failure if not running as root).
|
// Best-effort ownership fix (ignore failure if not running as root).
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
#!/usr/bin/env node
|
#!/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.
|
// 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 { readdirSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -34,5 +50,5 @@ for (const d of dirs) {
|
||||||
}
|
}
|
||||||
console.log(` • ${d.name}`);
|
console.log(` • ${d.name}`);
|
||||||
console.log(` ${title}${cat ? ` [${cat}]` : ""} cover:${hasCover ? "yes" : "NO"}`);
|
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`);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
#!/usr/bin/env node
|
#!/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.
|
// 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 { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -12,8 +28,8 @@ const PIPELINE = resolve(HERE, "..");
|
||||||
const cfg = loadJson(join(PIPELINE, "config.json"));
|
const cfg = loadJson(join(PIPELINE, "config.json"));
|
||||||
const root = cfg.paths.projectRoot;
|
const root = cfg.paths.projectRoot;
|
||||||
|
|
||||||
const queuePath = join(root, "content-pipeline", "news-queue.json");
|
const queuePath = join(root, cfg.paths.newsDir, "news-queue.json");
|
||||||
const scanPath = join(root, "content-pipeline", "news-scan.json");
|
const scanPath = join(root, cfg.paths.newsDir, "news-scan.json");
|
||||||
|
|
||||||
const loadArr = (p) => (existsSync(p) ? loadJson(p) : []);
|
const loadArr = (p) => (existsSync(p) ? loadJson(p) : []);
|
||||||
const publishedSlugs = () => {
|
const publishedSlugs = () => {
|
||||||
|
|
@ -1,9 +1,25 @@
|
||||||
#!/usr/bin/env node
|
#!/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
|
// 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
|
// 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
|
// 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.
|
// 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 { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -22,7 +38,7 @@ if (!cfg.publish?.auto) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = todayInTz(cfg.editorial.timezone);
|
const today = todayInTz(cfg.editorial.timezone);
|
||||||
const stateDir = join(root, "content-pipeline", "state");
|
const stateDir = join(root, cfg.paths.stateDir);
|
||||||
mkdirSync(stateDir, { recursive: true });
|
mkdirSync(stateDir, { recursive: true });
|
||||||
const statePath = join(stateDir, `publish-${today}.json`);
|
const statePath = join(stateDir, `publish-${today}.json`);
|
||||||
|
|
||||||
|
|
@ -1,7 +1,23 @@
|
||||||
#!/usr/bin/env node
|
#!/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).
|
// Editorial research: top up the EVERGREEN backlog in calendar.json (additive, deduped).
|
||||||
// Timely news is handled separately by the news radar. Usage:
|
// 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 { readdirSync, existsSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -26,7 +42,7 @@ export async function runResearch({ count } = {}) {
|
||||||
const n = count || cfg.editorial.calendarDays;
|
const n = count || cfg.editorial.calendarDays;
|
||||||
const today = todayInTz(cfg.editorial.timezone);
|
const today = todayInTz(cfg.editorial.timezone);
|
||||||
const calendarPath = join(root, cfg.paths.calendar);
|
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.
|
// Per-type target counts from the configured mix.
|
||||||
const types = Object.keys(cfg.editorial.contentMix);
|
const types = Object.keys(cfg.editorial.contentMix);
|
||||||
|
|
@ -1,7 +1,23 @@
|
||||||
#!/usr/bin/env node
|
#!/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
|
// 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.
|
// draft passes the gate or maxReviseAttempts is reached.
|
||||||
// Usage: node content-pipeline/scripts/revise.mjs <slug>
|
// Usage: node agents/scripts/revise.mjs <slug>
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -1,8 +1,24 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
// SEO Specialist agent: audit one article for EEAT / spam-policy / on-page SEO / AEO / readability.
|
// SEO Specialist agent: audit one article for EEAT / spam-policy / on-page SEO / AEO / readability.
|
||||||
// Writes <postDir>/seo-review.json. Usage:
|
// Writes <postDir>/seo-review.json. Usage:
|
||||||
// node scripts/seo-review.mjs <slug> [--published]
|
// node scripts/seo-review.mjs <slug> [--published]
|
||||||
// (default: review the draft in content-pipeline/drafts/<slug>; --published reviews the
|
// (default: review the draft in agents/drafts/<slug>; --published reviews the
|
||||||
// live post in app/src/content/blog/<slug>)
|
// live post in app/src/content/blog/<slug>)
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
|
|
@ -1,8 +1,24 @@
|
||||||
#!/usr/bin/env node
|
#!/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/<slug>/.
|
// Daily writer: draft one article (MDX + cover.jpg + sources.json) into drafts/<slug>/.
|
||||||
// Selection order: today's news (news radar) first → else the next evergreen calendar topic.
|
// 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.
|
// 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 { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, resolve, join } from "node:path";
|
import { dirname, resolve, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -26,7 +42,7 @@ if (cfg.author.slug === "REPLACE_ME") {
|
||||||
|
|
||||||
const calendarPath = join(root, cfg.paths.calendar);
|
const calendarPath = join(root, cfg.paths.calendar);
|
||||||
const today = todayInTz(cfg.editorial.timezone);
|
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];
|
const argSlug = process.argv[2];
|
||||||
let entry;
|
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}
|
- Draft output directory (write index.mdx, cover.jpg, sources.json here): ${draftDir}
|
||||||
- Existing published posts to read for internal links: ${blogDir}
|
- Existing published posts to read for internal links: ${blogDir}
|
||||||
- Image: use the Higgsfield MCP (preferred model "${cfg.image.preferredModel}", aspect ratio
|
- 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).
|
(recommend model → generate_image → job_status sync → curl the result URL).
|
||||||
Save the final image to: ${join(draftDir, "cover.jpg")}.
|
Save the final image to: ${join(draftDir, "cover.jpg")}.
|
||||||
${
|
${
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"console": {
|
"console": {
|
||||||
"port": 3011,
|
"port": 3011,
|
||||||
"route": "/devconsole",
|
"route": "/devconsole",
|
||||||
"tokenFile": "content-pipeline/.env",
|
"tokenFile": "agents/.env",
|
||||||
"tokenKey": "ADMIN_TOKEN"
|
"tokenKey": "ADMIN_TOKEN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@
|
||||||
*
|
*
|
||||||
* Writes:
|
* Writes:
|
||||||
* - app/src/config/site.json (Astro theme reads this)
|
* - 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)
|
* - 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 { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
@ -38,15 +40,15 @@ write("app/src/config/site.json", {
|
||||||
social: site.social,
|
social: site.social,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2) content-pipeline
|
// 2) agents
|
||||||
if (existsSync(resolve(root, "content-pipeline/config.json"))) {
|
if (existsSync(resolve(root, "agents/config.json"))) {
|
||||||
const cp = read("content-pipeline/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.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 };
|
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.paths) cp.paths.projectRoot = root; // this clone's absolute path, not comiida's
|
||||||
if (cp.editorial) cp.editorial.timezone = site.timezone;
|
if (cp.editorial) cp.editorial.timezone = site.timezone;
|
||||||
if (cp.image && cp.image.credit) cp.image.credit.author = site.name;
|
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
|
// 3) astroagent
|
||||||
|
|
@ -58,4 +60,10 @@ if (existsSync(resolve(root, "astroagent.config.json"))) {
|
||||||
write("astroagent.config.json", aa);
|
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)");
|
console.log("Done. Rebuild the site: (cd app && npm run build)");
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
# ./scripts/new-site.sh
|
# ./scripts/new-site.sh
|
||||||
#
|
#
|
||||||
# Prompts for this site's identity, writes site.config.json, stamps it across
|
# 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.
|
# git history and installs dependencies.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue