2026-07-04 22:53:10 +00:00
|
|
|
#!/usr/bin/env node
|
feat: content-pipeline/ → agents/ — formalize the agent system in the seed
Adopt the agents/ architecture proven on medellin.co (reference impl):
- Move the content engine to a top-level agents/ dir: orchestrators, prompts,
config, run.sh, admin console, shared libs. All content-pipeline literals
repointed (config paths, scripts, admin, LLM-facing prompts/image.md string,
configure.mjs, new-site.sh, astroagent tokenFile, .gitignore runtime block).
- Every script carries a parseable @agent-manifest header: name, title, class
(content|operational|runtime|plumbing), trigger, model, prompts, skills (MCP),
tools, reads/writes tables. 5 content agents + 3 plumbing scripts.
- New agents/catalog.mjs generates the catalog from the headers:
agents/AGENTS.md (human, grouped by class) + agents/agents.json (machine
manifest — a clone diffs it against a source to find missing tools/tables/MCP
before running). configure.mjs regenerates the catalog on every identity
stamp. No DB table, no watcher.
- config.json gains paths.stateDir/newsDir; publish-tick, write-daily, and
news-radar read them instead of hardcoding.
- Full cut: content-pipeline/ deleted (the seed has no live crons, so no
hybrid period needed). Docs updated (AGENTS.md structure + pipeline section,
README paths).
Clones migrating from content-pipeline/: see medellin.co's
.memory/handoffs/agents-directory-migration.md for the cutover playbook
(one cron set active at a time; migrate drafts/state after repointing cron).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G
2026-07-11 20:33:48 +00:00
|
|
|
/**
|
|
|
|
|
* @agent-manifest
|
|
|
|
|
* name: 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
|
|
|
|
|
*/
|
2026-07-04 22:53:10 +00:00
|
|
|
// 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)
|
2026-07-04 22:53:10 +00:00
|
|
|
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);
|
2026-07-04 22:53:10 +00:00
|
|
|
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);
|
|
|
|
|
}
|