seedproject-web/agents/scripts/research.mjs

111 lines
4.2 KiB
JavaScript
Raw Normal View History

#!/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: 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:
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
// node agents/scripts/research.mjs [count]
import { readdirSync, existsSync } from "node:fs";
import { dirname, resolve, join } from "node:path";
import { fileURLToPath } from "node:url";
import { runClaude } from "./lib/claude.mjs";
import { loadJson, todayInTz, addDays } from "./lib/util.mjs";
import { readCalendar, mergeEntries } from "./lib/calendar.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const PIPELINE = resolve(HERE, "..");
const cfg = loadJson(join(PIPELINE, "config.json"));
const root = cfg.paths.projectRoot;
const publishedSlugs = () => {
const d = join(root, cfg.paths.blogContentDir);
return existsSync(d)
? readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => x.name)
: [];
};
/** Scan for evergreen topics and additively merge them into calendar.json. Returns count added. */
export async function runResearch({ count } = {}) {
const n = count || cfg.editorial.calendarDays;
const today = todayInTz(cfg.editorial.timezone);
const calendarPath = join(root, cfg.paths.calendar);
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
const scanPath = join(root, "agents", "research-scan.json");
// Per-type target counts from the configured mix.
const types = Object.keys(cfg.editorial.contentMix);
const counts = {};
let assigned = 0;
for (const t of types) {
counts[t] = Math.round(cfg.editorial.contentMix[t] * n);
assigned += counts[t];
}
const biggest = types.reduce((a, b) => (counts[a] >= counts[b] ? a : b));
counts[biggest] += n - assigned;
const exclude = [...new Set([...readCalendar(calendarPath).map((e) => e.slug), ...publishedSlugs()])];
const prompt = `Propose ${n} evergreen Comiida topics.
- Write a JSON array of ${n} topic proposals to: ${scanPath}
- Do NOT assign dates (the pipeline schedules them).
- Allowed content types: ${types.join(", ")}
- Rough target counts per type: ${JSON.stringify(counts)}
- DO NOT reuse any of these slugs: ${exclude.join(", ") || "(none yet)"}
- Read existing posts if useful: ${join(root, cfg.paths.blogContentDir)}
Follow the output contract in your system prompt exactly.`;
console.log(`[research] proposing ${n} evergreen topics, mix ${JSON.stringify(counts)}`);
await runClaude({
prompt,
systemPromptFile: join(PIPELINE, "prompts/research.system.md"),
model: cfg.models.research,
allowedTools: "Read Glob Grep WebSearch Write",
addDirs: [root],
cwd: root,
logFile: join(root, cfg.paths.logsDir, `research-${today}.log`),
});
const proposals = existsSync(scanPath) ? loadJson(scanPath) : [];
if (!proposals.length) {
console.log("[research] no proposals produced.");
return 0;
}
// Schedule new topics one per day, starting the day after the latest existing date.
const dates = readCalendar(calendarPath).map((e) => (e.date || "").slice(0, 10)).filter(Boolean);
const maxDate = dates.sort().pop();
let next = maxDate && maxDate >= today ? addDays(maxDate, 1) : today;
const entries = proposals.map((p) => {
const entry = { ...p, date: next, status: "planned" };
next = addDays(next, 1);
return entry;
});
const added = await mergeEntries(calendarPath, entries);
const planned = readCalendar(calendarPath).filter((e) => e.status === "planned").length;
console.log(`[research] added ${added} evergreen topics. Planned backlog now: ${planned}.`);
return added;
}
// CLI
if (import.meta.url === `file://${process.argv[1]}`) {
const count = parseInt(process.argv[2], 10) || undefined;
await runResearch({ count });
}