diff --git a/agents/AGENTS.md b/agents/AGENTS.md index 42179d4..a007868 100644 --- a/agents/AGENTS.md +++ b/agents/AGENTS.md @@ -21,6 +21,17 @@ Cron/manual-triggered producers of gated drafts (events auto-publishes). | **seo-review** ([agents/scripts/seo-review.mjs](../agents/scripts/seo-review.mjs)) | manual (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. | +## runtime + +HTTP-triggered agents answering live user requests. + +| agent | trigger | model | skills | tools | reads → writes | description | +|---|---|---|---|---|---|---| +| **in-page-console** ([agents/console/agents/in-page-console.mjs](../agents/console/agents/in-page-console.mjs)) | POST /run; GET /stream; POST /publish; POST /rebuild; POST /discard | claude-sonnet-4-6 | — | Read, Write, Edit, Glob, Grep, WebSearch | — → — | Freeform ▲ editor: describe a change on any page; the agent edits, the console builds an isolated preview, then publish/discard. | +| **project-builder** ([agents/console/agents/project-builder.mjs](../agents/console/agents/project-builder.mjs)) | POST /draft; POST /build-project | claude-sonnet-4-6 | — | Read | cja_projects → cja_projects | Drafts copy and authors a full structured project record (reads uploaded images) for the New Project admin form. Returns JSON; never mutates the repo. | +| **qa** ([agents/console/agents/qa.mjs](../agents/console/agents/qa.mjs)) | POST /qa/run; schedule(heartbeatMin) | claude-sonnet-4-6 | qa | Read, Skill, api/cli/qa-start.php, api/cli/qa-finish.php, api/cli/qa-routes.php, api/cli/qa-autofix.php | cja_qa_runs, cja_qa_findings → cja_qa_runs, cja_qa_findings | Crawls the live site (links, images, forms, API, SEO/meta, a11y); auto-fixes safe findings via the Web Designer queue; runs on a heartbeat. | +| **web-designer** ([agents/console/agents/web-designer.mjs](../agents/console/agents/web-designer.mjs)) | POST /tasks/run | claude-sonnet-4-6 | brand, ui-ux, changelog | Read, Write, Edit, Glob, Grep, WebSearch, Skill, api/cli/tasks-next.php, api/cli/tasks-finish.php, api/cli/seed-changelog.php | cja_tasks, cja_changelog, cja_projects → cja_tasks, cja_changelog, cja_projects | Durable design queue (cja_tasks): runs the full-builder agent on any page from a brief, auto-publishes (build-gated scoped commit), and self-logs to the changelog. Fed by the admin and by QA autofix. | + ## plumbing Deterministic helpers — not agents (no LLM), catalogued for completeness. diff --git a/agents/agents.json b/agents/agents.json index 4875a4f..ca9a5aa 100644 --- a/agents/agents.json +++ b/agents/agents.json @@ -17,6 +17,28 @@ "writes": [], "created": "2026-07-04" }, + { + "file": "agents/console/agents/in-page-console.mjs", + "name": "in-page-console", + "title": "In-page Console", + "class": "runtime", + "trigger": "POST /run; GET /stream; POST /publish; POST /rebuild; POST /discard", + "model": "claude-sonnet-4-6", + "description": "Freeform ▲ editor: describe a change on any page; the agent edits, the console builds an isolated preview, then publish/discard.", + "prompt": [], + "skills": [], + "tools": [ + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "WebSearch" + ], + "reads": [], + "writes": [], + "created": "" + }, { "file": "agents/scripts/list-drafts.mjs", "name": "list-drafts", @@ -51,6 +73,27 @@ "writes": [], "created": "2026-07-04" }, + { + "file": "agents/console/agents/project-builder.mjs", + "name": "project-builder", + "title": "Project Builder", + "class": "runtime", + "trigger": "POST /draft; POST /build-project", + "model": "claude-sonnet-4-6", + "description": "Drafts copy and authors a full structured project record (reads uploaded images) for the New Project admin form. Returns JSON; never mutates the repo.", + "prompt": [], + "skills": [], + "tools": [ + "Read" + ], + "reads": [ + "cja_projects" + ], + "writes": [ + "cja_projects" + ], + "created": "" + }, { "file": "agents/scripts/publish-tick.mjs", "name": "publish-tick", @@ -68,6 +111,36 @@ "writes": [], "created": "2026-07-04" }, + { + "file": "agents/console/agents/qa.mjs", + "name": "qa", + "title": "QA Agent", + "class": "runtime", + "trigger": "POST /qa/run; schedule(heartbeatMin)", + "model": "claude-sonnet-4-6", + "description": "Crawls the live site (links, images, forms, API, SEO/meta, a11y); auto-fixes safe findings via the Web Designer queue; runs on a heartbeat.", + "prompt": [], + "skills": [ + "qa" + ], + "tools": [ + "Read", + "Skill", + "api/cli/qa-start.php", + "api/cli/qa-finish.php", + "api/cli/qa-routes.php", + "api/cli/qa-autofix.php" + ], + "reads": [ + "cja_qa_runs", + "cja_qa_findings" + ], + "writes": [ + "cja_qa_runs", + "cja_qa_findings" + ], + "created": "" + }, { "file": "agents/scripts/research.mjs", "name": "research", @@ -126,6 +199,44 @@ "writes": [], "created": "2026-07-04" }, + { + "file": "agents/console/agents/web-designer.mjs", + "name": "web-designer", + "title": "Web Designer", + "class": "runtime", + "trigger": "POST /tasks/run", + "model": "claude-sonnet-4-6", + "description": "Durable design queue (cja_tasks): runs the full-builder agent on any page from a brief, auto-publishes (build-gated scoped commit), and self-logs to the changelog. Fed by the admin and by QA autofix.", + "prompt": [], + "skills": [ + "brand", + "ui-ux", + "changelog" + ], + "tools": [ + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "WebSearch", + "Skill", + "api/cli/tasks-next.php", + "api/cli/tasks-finish.php", + "api/cli/seed-changelog.php" + ], + "reads": [ + "cja_tasks", + "cja_changelog", + "cja_projects" + ], + "writes": [ + "cja_tasks", + "cja_changelog", + "cja_projects" + ], + "created": "" + }, { "file": "agents/scripts/write-daily.mjs", "name": "writer", diff --git a/agents/catalog.mjs b/agents/catalog.mjs index fe2f01b..1a00bc2 100644 --- a/agents/catalog.mjs +++ b/agents/catalog.mjs @@ -10,13 +10,14 @@ // (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"; +import { fileURLToPath, pathToFileURL } 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 +// Runtime agents living outside agents/scripts (the console runner's pluggable +// modules) — discovered from their JS `manifest` export (see the loop below). +const RUNTIME_FILES = []; // legacy seam for PHP runtime agents; console agents handled below const CLASSES = ["content", "operational", "runtime", "plumbing"]; const LIST_KEYS = ["prompt", "skills", "tools", "reads", "writes"]; @@ -56,6 +57,35 @@ for (const rel of files.sort()) { } agents.push({ file: rel, ...manifest }); } + +// ---- runtime (console) agents: JS `manifest` exports ---- +// Each agents/console/agents/*.mjs exports a `manifest` object. Importing a +// module only defines it (register()/listen() aren't called), so this is safe. +const consoleDir = join(HERE, "console", "agents"); +let aiModel = "-"; +try { aiModel = JSON.parse(readFileSync(join(ROOT, "astroagent.config.json"), "utf8")).ai?.model || "-"; } catch {} +let consoleFiles = []; +try { consoleFiles = readdirSync(consoleDir).filter((f) => f.endsWith(".mjs")); } catch {} +for (const f of consoleFiles.sort()) { + const rel = ["agents", "console", "agents", f].join("/"); + let mod; + try { mod = await import(pathToFileURL(join(consoleDir, f)).href); } + catch (e) { console.error(`[catalog] ${rel}: import failed — ${e.message}`); continue; } + const m = mod.manifest; + if (!m || !m.name || !m.class) continue; + const trigger = (m.triggers || []).map((t) => + t.type === "endpoint" ? `${t.method} ${t.path}` : t.type === "schedule" ? `schedule(${t.settingKey})` : t.type + ).join("; "); + const toolsArr = typeof m.tools === "string" ? m.tools.split(/\s+/).filter(Boolean) : (m.tools || []); + agents.push({ + file: rel, name: m.name, title: m.title || m.name, class: m.class, + trigger, model: aiModel, description: m.description || "", + prompt: [], skills: m.skills || [], + tools: [...toolsArr, ...(m.cli || []).map((c) => `api/cli/${c}.php`)], + reads: m.tables || [], writes: m.tables || [], created: "", + }); +} + agents.sort((a, b) => a.name.localeCompare(b.name)); // ---- agents.json (machine manifest) ---- diff --git a/agents/console/agents/in-page-console.mjs b/agents/console/agents/in-page-console.mjs index 3f6de38..af54552 100644 --- a/agents/console/agents/in-page-console.mjs +++ b/agents/console/agents/in-page-console.mjs @@ -7,6 +7,19 @@ import { site } from "../site.mjs"; +export const manifest = { + name: "in-page-console", title: "In-page Console", class: "runtime", + description: "Freeform ▲ editor: describe a change on any page; the agent edits, the console builds an isolated preview, then publish/discard.", + triggers: [ + { type: "endpoint", method: "POST", path: "/run" }, + { type: "endpoint", method: "GET", path: "/stream" }, + { type: "endpoint", method: "POST", path: "/publish" }, + { type: "endpoint", method: "POST", path: "/rebuild" }, + { type: "endpoint", method: "POST", path: "/discard" }, + ], + tools: site.tools, skills: [], tables: [], cli: [], +}; + let H; // the host, set in register() function buildPrompt({ message, page, selections }) { @@ -103,7 +116,7 @@ function runAgent(job, { message, page, selections, model }) { }); } -export function register(host) { +export function register(host, settings = {}) { H = host; host.route("POST", "/run", async (req, res) => { diff --git a/agents/console/agents/project-builder.mjs b/agents/console/agents/project-builder.mjs index 6cae604..1bbf2b6 100644 --- a/agents/console/agents/project-builder.mjs +++ b/agents/console/agents/project-builder.mjs @@ -6,6 +6,16 @@ import { site } from "../site.mjs"; +export const manifest = { + name: "project-builder", title: "Project Builder", class: "runtime", + description: "Drafts copy and authors a full structured project record (reads uploaded images) for the New Project admin form. Returns JSON; never mutates the repo.", + triggers: [ + { type: "endpoint", method: "POST", path: "/draft" }, + { type: "endpoint", method: "POST", path: "/build-project" }, + ], + tools: "Read", skills: [], tables: ["cja_projects"], cli: [], +}; + let H; const MEDIA_RE = /^\/media\/[\w.-]+$/; @@ -114,7 +124,7 @@ async function buildProject({ name, category, draft, prompt, images, instagram } } catch { return { ok: false }; } } -export function register(host) { +export function register(host, settings = {}) { H = host; host.route("POST", "/draft", async (req, res) => { diff --git a/agents/console/agents/qa.mjs b/agents/console/agents/qa.mjs index 0dedfbc..71f0888 100644 --- a/agents/console/agents/qa.mjs +++ b/agents/console/agents/qa.mjs @@ -11,11 +11,24 @@ import { site } from "../site.mjs"; let H; +export const manifest = { + name: "qa", title: "QA Agent", class: "runtime", + description: "Crawls the live site (links, images, forms, API, SEO/meta, a11y); auto-fixes safe findings via the Web Designer queue; runs on a heartbeat.", + triggers: [ + { type: "endpoint", method: "POST", path: "/qa/run" }, + { type: "schedule", settingKey: "heartbeatMin" }, + ], + tools: "Read Skill", skills: ["qa"], + tables: ["cja_qa_runs", "cja_qa_findings"], + cli: ["qa-start", "qa-finish", "qa-routes", "qa-autofix"], +}; + const QA_BASE = site.url; const QA_UA = `Mozilla/5.0 (compatible; AstroAgentQA/1.0; +${site.url})`; const QA_STATIC_ROUTES = site.qaRoutes; -const QA_AUTOFIX = (process.env.QA_AUTOFIX ?? "1") !== "0"; -const QA_HEARTBEAT_MIN = Number(process.env.QA_HEARTBEAT_MIN || 60); +const QA_AUTOFIX_ENV = (process.env.QA_AUTOFIX ?? "1") !== "0"; +const QA_HEARTBEAT_ENV = Number(process.env.QA_HEARTBEAT_MIN || 60); +let qaAutofix = QA_AUTOFIX_ENV; // effective; set from settings in register() let qaRunning = false; async function probe(url, { method = "GET", readBody = false, timeout = 12000, headers = {}, body = null } = {}) { @@ -203,7 +216,7 @@ async function runQaFlow(trigger) { await H.phpCli(["api/cli/qa-finish.php", `--run=${runId}`, "--status=done", `--summary=${summary}`, `--counts=${JSON.stringify(counts)}`, `--findings-file=${tmp}`]); rmSync(tmp, { force: true }); - if (QA_AUTOFIX) { + if (qaAutofix) { const af = await H.phpCli(["api/cli/qa-autofix.php", `--run=${runId}`]); let queued = 0; try { queued = JSON.parse(af.out || "{}").queued || 0; } catch {} if (queued > 0 && H.drainTasks) H.drainTasks(); // fire-and-forget @@ -215,8 +228,10 @@ async function runQaFlow(trigger) { } } -export function register(host) { +export function register(host, settings = {}) { H = host; + qaAutofix = settings.autofix ?? QA_AUTOFIX_ENV; + const heartbeatMin = settings.heartbeatMin ?? QA_HEARTBEAT_ENV; host.route("POST", "/qa/run", async (req, res) => { if (qaRunning) return H.json(res, 429, { error: "A QA run is already in progress." }); @@ -226,8 +241,8 @@ export function register(host) { H.json(res, 200, { ok: true }); }); - if (QA_HEARTBEAT_MIN > 0) { - host.everyMinutes(QA_HEARTBEAT_MIN, () => runQaFlow("scheduled"), { bootDelayMs: 90_000 }); - console.log(`[qa] heartbeat every ${QA_HEARTBEAT_MIN} min`); + if (heartbeatMin > 0) { + host.everyMinutes(heartbeatMin, () => runQaFlow("scheduled"), { bootDelayMs: 90_000 }); + console.log(`[qa] heartbeat every ${heartbeatMin} min`); } } diff --git a/agents/console/agents/web-designer.mjs b/agents/console/agents/web-designer.mjs index 06f8cc5..195d0c5 100644 --- a/agents/console/agents/web-designer.mjs +++ b/agents/console/agents/web-designer.mjs @@ -7,6 +7,16 @@ import { site } from "../site.mjs"; +export const manifest = { + name: "web-designer", title: "Web Designer", class: "runtime", + description: "Durable design queue (cja_tasks): runs the full-builder agent on any page from a brief, auto-publishes (build-gated scoped commit), and self-logs to the changelog. Fed by the admin and by QA autofix.", + triggers: [{ type: "endpoint", method: "POST", path: "/tasks/run" }], + tools: "Read Write Edit Glob Grep WebSearch Skill", + skills: ["brand", "ui-ux", "changelog"], + tables: ["cja_tasks", "cja_changelog", "cja_projects"], + cli: ["tasks-next", "tasks-finish", "seed-changelog"], +}; + let H; let draining = false; @@ -146,7 +156,7 @@ async function drainTasks() { } } -export function register(host) { +export function register(host, settings = {}) { H = host; host.drainTasks = drainTasks; // QA autofix kicks the queue through this diff --git a/agents/console/server.mjs b/agents/console/server.mjs index 07b0d21..95ef8e7 100644 --- a/agents/console/server.mjs +++ b/agents/console/server.mjs @@ -2,27 +2,32 @@ /** * AstroAgent console runner — bootstrap. * - * Loads the generic host, registers the agent modules, and starts listening. - * Runs AS the confined agent user (systemd User=), binds 127.0.0.1 only, and is - * reached exclusively through nginx, which gates every request with auth_request - * against the PHP admin session — this process does not re-check that. + * Loads the generic host, then registers ONLY the agents this project enables + * (astroagent.config.json → agents.enabled, exposed via site.agents), passing + * each its per-agent settings. Each capability is a module under ./agents/ + * exporting register(host, settings). * - * The shared runtime lives in ./host.mjs; each capability is a module under - * ./agents/ exporting register(host). (Phase 3 will drive this roster from - * astroagent.config.json instead of the hardcoded list below.) - * - * Node built-ins only — no dependencies. + * Runs AS the confined agent user, binds 127.0.0.1, reached only through nginx + * (auth_request against the PHP admin session). Node built-ins only. */ import { host } from "./host.mjs"; -import { register as registerInPageConsole } from "./agents/in-page-console.mjs"; -import { register as registerProjectBuilder } from "./agents/project-builder.mjs"; -import { register as registerWebDesigner } from "./agents/web-designer.mjs"; -import { register as registerQa } from "./agents/qa.mjs"; +import { site } from "./site.mjs"; -registerInPageConsole(host); -registerProjectBuilder(host); -registerWebDesigner(host); -registerQa(host); +const { enabled, settings } = site.agents; + +for (const name of enabled) { + try { + const mod = await import(`./agents/${name}.mjs`); + if (typeof mod.register === "function") { + mod.register(host, settings[name] || {}); + console.log(`[console] agent enabled: ${name}`); + } else { + console.warn(`[console] agent '${name}' has no register() — skipped`); + } + } catch (err) { + console.error(`[console] agent '${name}' failed to load: ${err.message}`); + } +} host.listen(); diff --git a/agents/console/site.mjs b/agents/console/site.mjs index 827c556..8c84a56 100644 --- a/agents/console/site.mjs +++ b/agents/console/site.mjs @@ -35,9 +35,15 @@ export const site = { audience: sc.audience || "", qaEmail: `qa@${host}`, // Static route seed for the QA crawler (it also discovers via links + sitemap - // + qa-routes.php). Site-derived; Phase 3 moves this to agents.settings.qa. - qaRoutes: [ + // + qa-routes.php). Site-derived; overridable via agents.settings.qa.routes. + qaRoutes: aa.agents?.settings?.qa?.routes || [ "/", "/about", "/services", "/services/website-design", "/projects", "/blog", "/contact", "/changelog", "/resume", "/faq", ], + // Per-project agent roster: which console agents run here + their settings. + // Defaults to all four (back-compat) when astroagent.config.json omits it. + agents: { + enabled: aa.agents?.enabled || ["in-page-console", "project-builder", "web-designer", "qa"], + settings: aa.agents?.settings || {}, + }, };