seedproject-web/agents/scripts/publish-tick.mjs

118 lines
4.6 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: 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.
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
// 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";
import { loadJson, todayInTz, randomTimestampTodaySoFar } from "./lib/util.mjs";
import { promoteDraft } from "./lib/publish.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 FORCE_NOW = process.argv.includes("--now");
if (!cfg.publish?.auto) {
console.log("[publish-tick] publish.auto is false — nothing to do.");
process.exit(0);
}
const today = todayInTz(cfg.editorial.timezone);
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 stateDir = join(root, cfg.paths.stateDir);
mkdirSync(stateDir, { recursive: true });
const statePath = join(stateDir, `publish-${today}.json`);
let state = existsSync(statePath) ? loadJson(statePath) : { published: false, slug: null };
if (state.published) {
console.log(`[publish-tick] already published today (${state.slug}).`);
process.exit(0);
}
// Morning floor: don't publish before this Bogotá time (gives the 06:00 writer time to draft +
// audit + auto-revise so the day's freshest article is the one that goes live).
const notBefore = cfg.publish.notBefore || "07:00";
const floorMs = Date.parse(`${today}T${notBefore}:00-05:00`);
if (!FORCE_NOW && Date.now() < floorMs) {
console.log(`[publish-tick] waiting — floor ${notBefore} Bogotá (${new Date(floorMs).toISOString()}).`);
process.exit(0);
}
// Find an eligible draft: on disk, has cover, and passes the SEO gate.
const minScore = cfg.seo?.minScore ?? 85;
const draftsRoot = join(root, cfg.paths.draftsDir);
const calPath = join(root, cfg.paths.calendar);
const calendar = existsSync(calPath) ? loadJson(calPath) : [];
const entryOf = (slug) => calendar.find((e) => e.slug === slug) || {};
const dateOf = (slug) => entryOf(slug).date || "9999-12-31";
function eligible(slug) {
const d = join(draftsRoot, slug);
if (!existsSync(join(d, "index.mdx")) || !existsSync(join(d, "cover.jpg"))) return false;
const auditPath = join(d, "seo-review.json");
if (!existsSync(auditPath)) return false;
try {
const a = loadJson(auditPath);
return a.verdict !== "fail" && (a.overall ?? 0) >= minScore;
} catch {
return false;
}
}
const candidates = existsSync(draftsRoot)
? readdirSync(draftsRoot, { withFileTypes: true })
.filter((x) => x.isDirectory())
.map((x) => x.name)
.filter(eligible)
// News drafts first (hottest, then by date); evergreen after, oldest first.
.sort((a, b) => {
const ea = entryOf(a);
const eb = entryOf(b);
const na = ea.news ? 1 : 0;
const nb = eb.news ? 1 : 0;
if (na !== nb) return nb - na;
if (na) return (eb.freshnessScore ?? 0) - (ea.freshnessScore ?? 0) || dateOf(a).localeCompare(dateOf(b));
return dateOf(a).localeCompare(dateOf(b));
})
: [];
if (!candidates.length) {
console.log("[publish-tick] no SEO-passing draft ready to publish (leaving queue as-is).");
process.exit(0);
}
const slug = candidates[0];
const stamp = randomTimestampTodaySoFar(cfg.editorial.timezone); // morning go-live, random timestamp
console.log(`[publish-tick] auto-publishing "${slug}" with timestamp ${stamp}`);
try {
const { url, taxonomyAdded } = await promoteDraft(cfg, slug, { dateOverride: stamp });
if (taxonomyAdded.length) console.log(`[publish-tick] registered: ${taxonomyAdded.join(", ")}`);
state = { published: true, slug, publishedAt: stamp };
writeFileSync(statePath, JSON.stringify(state, null, 2));
console.log(`[publish-tick] LIVE → ${url}`);
} catch (e) {
console.error(`[publish-tick] publish failed: ${e.message}`);
process.exit(1);
}