seedproject-web/agents/scripts/lib/calendar.mjs
Carlos Arias 2c969c0753 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 17:09:15 -05:00

95 lines
2.8 KiB
JavaScript

// Atomic, locked access to calendar.json so concurrent runs (write-daily, publish-tick,
// research refill) can't clobber each other. Every mutation re-reads under an exclusive
// lockfile, mutates, and writes — no long-held in-memory copies.
import { existsSync, readFileSync, writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs";
const LOCK_STALE_MS = 10 * 60 * 1000; // a lock older than this is presumed orphaned
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function acquire(calPath, timeoutMs = 120000) {
const lp = `${calPath}.lock`;
const start = Date.now();
for (;;) {
try {
const fd = openSync(lp, "wx"); // exclusive create — fails if it exists
writeFileSync(lp, `${process.pid} ${new Date().toISOString()}`);
closeSync(fd);
return lp;
} catch {
try {
if (Date.now() - statSync(lp).mtimeMs > LOCK_STALE_MS) {
unlinkSync(lp);
continue;
}
} catch {
/* lock vanished — retry */
}
if (Date.now() - start > timeoutMs) throw new Error(`calendar lock timeout: ${lp}`);
await sleep(200);
}
}
}
const release = (lp) => {
try {
unlinkSync(lp);
} catch {
/* already gone */
}
};
export async function withCalendarLock(calPath, fn) {
const lp = await acquire(calPath);
try {
return await fn();
} finally {
release(lp);
}
}
export const readCalendar = (calPath) =>
existsSync(calPath) ? JSON.parse(readFileSync(calPath, "utf8")) : [];
const writeCalendar = (calPath, cal) => writeFileSync(calPath, JSON.stringify(cal, null, 2));
/** Locked read-modify-write of one entry (Object.assign patch). Returns the updated entry. */
export async function updateEntry(calPath, slug, patch) {
return withCalendarLock(calPath, () => {
const cal = readCalendar(calPath);
const e = cal.find((x) => x.slug === slug);
if (e) {
Object.assign(e, patch);
writeCalendar(calPath, cal);
}
return e;
});
}
/** Locked append if the slug is not already present. */
export async function addEntry(calPath, entry) {
return withCalendarLock(calPath, () => {
const cal = readCalendar(calPath);
if (!cal.some((x) => x.slug === entry.slug)) {
cal.push(entry);
writeCalendar(calPath, cal);
}
return entry;
});
}
/** Locked merge: append each new entry whose slug isn't present. Returns count added. */
export async function mergeEntries(calPath, entries) {
return withCalendarLock(calPath, () => {
const cal = readCalendar(calPath);
const have = new Set(cal.map((x) => x.slug));
let added = 0;
for (const e of entries) {
if (!have.has(e.slug)) {
cal.push(e);
have.add(e.slug);
added++;
}
}
writeCalendar(calPath, cal);
return added;
});
}