framework(core): lift site identity into site.mjs (config-driven)
Phase 2. host + agent modules no longer hardcode identity: - site.mjs derives name/url/host/model/agentUser/home/role/qaEmail from site.config.json + astroagent.config.json - host uses site.model/site.tools/site.home; QA uses site.url/host/qaEmail/routes - personas templated from site.*; site-specific brand wording (sumi-e, .ca-*, 'law firms') dropped — brand now comes only from brand/BRAND.md + the brand skill Core (host.mjs + agents/*) is now site-agnostic; identity lives in config.
This commit is contained in:
parent
542858b1ec
commit
7799fd62e3
6 changed files with 72 additions and 25 deletions
|
|
@ -5,15 +5,16 @@
|
|||
* revert. Owns: /run, /stream (SSE), /publish, /rebuild, /discard.
|
||||
*/
|
||||
|
||||
import { site } from "../site.mjs";
|
||||
|
||||
let H; // the host, set in register()
|
||||
|
||||
function buildPrompt({ message, page, selections }) {
|
||||
const parts = [];
|
||||
parts.push(
|
||||
"You are the editing agent for the Carlos Arias website (an Astro + Tailwind v4 static site).",
|
||||
`You are the editing agent for the ${site.name} website (an Astro + Tailwind v4 static site).`,
|
||||
"Make ONLY the change the operator asks for. Keep everything on-brand:",
|
||||
"the brand guide is brand/BRAND.md and the design tokens are in app/src/styles.css",
|
||||
"(sumi-e — ink on washi paper, one vermillion seal, restraint, near-square radii).",
|
||||
"the brand guide is brand/BRAND.md and the design tokens are in app/src/styles.css — read them and match the existing components.",
|
||||
"Do NOT run builds or git commands — the console builds and publishes for you.",
|
||||
"",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
* it via the PHP API); never mutates the repo. Owns: /draft, /build-project.
|
||||
*/
|
||||
|
||||
import { site } from "../site.mjs";
|
||||
|
||||
let H;
|
||||
|
||||
const MEDIA_RE = /^\/media\/[\w.-]+$/;
|
||||
|
|
@ -17,7 +19,7 @@ function stripFence(t) {
|
|||
// Turn a rough draft + prompt into polished project copy. No tools.
|
||||
async function draftCopy({ name, category, draft, prompt }) {
|
||||
const ask = [
|
||||
"You are writing copy for Carlos Arias's portfolio (carlosarias.co) — an Agentic AI & Automation Engineer who builds for law firms and service businesses.",
|
||||
`You are writing copy for ${site.name}'s portfolio (${site.host})${site.author.role ? " — " + site.author.role : ""}.`,
|
||||
"Voice: precise, understated, confident. No hype, no buzzwords, no exclamation marks. Write a project case study.",
|
||||
"",
|
||||
`Project name: ${name}`,
|
||||
|
|
@ -48,10 +50,10 @@ async function buildProject({ name, category, draft, prompt, images, instagram }
|
|||
const imageLines = paths.length ? paths.map((p, i) => ` ${i + 1}. ${p}`).join("\n") : "";
|
||||
|
||||
const ask = [
|
||||
"You are the Web Designer for Carlos Arias's portfolio (carlosarias.co) — an Agentic AI & Automation Engineer who builds for law firms and service businesses.",
|
||||
`You are the Web Designer for ${site.name}'s portfolio (${site.host})${site.author.role ? " — " + site.author.role : ""}.`,
|
||||
"Your job: turn the brief below into a COMPLETE project case study for a fixed, on-brand page template. You author STRUCTURED CONTENT, not HTML or layout.",
|
||||
"",
|
||||
"Brand (from brand/BRAND.md — honor it): sumi-e, ink on washi paper, a single vermillion seal, restraint. Voice is precise, understated, confident — no hype, no buzzwords, no exclamation marks. Never describe colours or layout; the template owns all styling.",
|
||||
"Brand: honor the brand guide in brand/BRAND.md. Voice is precise, understated, confident — no hype, no buzzwords, no exclamation marks. Never describe colours or layout; the template owns all styling.",
|
||||
"",
|
||||
"The page auto-renders these sections from the fields you return — populate the ones the brief supports, leave the rest empty:",
|
||||
"- header: title + one-line lede",
|
||||
|
|
@ -81,7 +83,7 @@ async function buildProject({ name, category, draft, prompt, images, instagram }
|
|||
'- "body": the Markdown case study.',
|
||||
'- "kind": short project type, e.g. "Website" or "SaaS / Media" (or "").',
|
||||
'- "period": a timeline label, e.g. "2024 — present" (or "").',
|
||||
'- "role": Carlos\'s role on the project (or "").',
|
||||
`- "role": ${site.author.name}'s role on the project (or "").`,
|
||||
'- "categories": array of 1-3 short strings.',
|
||||
'- "stack": array of technology strings.',
|
||||
'- "skills": array of {"group": string, "items": [string, ...]}.',
|
||||
|
|
|
|||
|
|
@ -7,15 +7,13 @@
|
|||
*/
|
||||
|
||||
import { writeFileSync, rmSync } from "node:fs";
|
||||
import { site } from "../site.mjs";
|
||||
|
||||
let H;
|
||||
|
||||
const QA_BASE = "https://carlosarias.co";
|
||||
const QA_UA = "Mozilla/5.0 (compatible; CarlosAriasQA/1.0; +https://carlosarias.co)";
|
||||
const QA_STATIC_ROUTES = [
|
||||
"/", "/about", "/services", "/services/website-design",
|
||||
"/projects", "/blog", "/contact", "/changelog", "/resume", "/faq",
|
||||
];
|
||||
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);
|
||||
let qaRunning = false;
|
||||
|
|
@ -91,7 +89,7 @@ async function runQa() {
|
|||
if (!title) add("seo", "warning", path, "Missing <title>.", `${path} has no <title>. Add a page-specific title via its BaseLayout props.`);
|
||||
else { if (!titles.has(title)) titles.set(title, []); titles.get(title).push(path); }
|
||||
if (!desc) add("seo", "warning", path, "Missing meta description.", `${path} has no meta description. Add a page-specific description via its BaseLayout props.`);
|
||||
if (canon) { try { const h = new URL(canon).host; if (h && h !== "carlosarias.co") add("seo", "error", path, `Canonical points to ${h}.`, `${path} canonical points to ${h} instead of carlosarias.co. Fix the site URL / canonical.`); } catch {} }
|
||||
if (canon) { try { const h = new URL(canon).host; if (h && h !== site.host) add("seo", "error", path, `Canonical points to ${h}.`, `${path} canonical points to ${h} instead of ${site.host}. Fix the site URL / canonical.`); } catch {} }
|
||||
if (!/<html[^>]+lang=/i.test(html)) add("a11y", "warning", path, "<html> has no lang attribute.", `${path} <html> tag has no lang attribute. Add lang="en".`);
|
||||
|
||||
for (const href of qaGrabAll(/<a\b[^>]*\bhref=["']([^"']+)["']/gi, html)) {
|
||||
|
|
@ -139,13 +137,13 @@ async function runQa() {
|
|||
const cHeaders = { "content-type": "application/json", origin: QA_BASE, referer: QA_BASE + "/contact" };
|
||||
const hp = await probe(QA_BASE + "/api/contact/submit", {
|
||||
method: "POST", readBody: true, headers: cHeaders,
|
||||
body: JSON.stringify({ name: "QA Bot", email: "qa@carlosarias.co", subject: "other", message: "QA honeypot probe — please ignore.", company: "qa-honeypot" }),
|
||||
body: JSON.stringify({ name: "QA Bot", email: site.qaEmail, subject: "other", message: "QA honeypot probe — please ignore.", company: "qa-honeypot" }),
|
||||
});
|
||||
if (hp.status !== 200) add("form", "error", "/api/contact/submit", `Contact honeypot probe returned ${hp.status || hp.error} (expected 200).`,
|
||||
`POST /api/contact/submit returned ${hp.status || hp.error} instead of 200 for a probe. The contact form endpoint may be broken — check api/public/controllers/contact.php.`);
|
||||
const val = await probe(QA_BASE + "/api/contact/submit", {
|
||||
method: "POST", readBody: true, headers: cHeaders,
|
||||
body: JSON.stringify({ name: "QA", email: "qa@carlosarias.co", subject: "other", message: "hi" }),
|
||||
body: JSON.stringify({ name: "QA", email: site.qaEmail, subject: "other", message: "hi" }),
|
||||
});
|
||||
if (val.status !== 422) add("form", "warning", "/api/contact/submit", `Validation probe returned ${val.status || val.error} (expected 422 for a too-short message).`,
|
||||
`POST /api/contact/submit did not reject an invalid submission (got ${val.status || val.error}, expected 422). Server-side validation may be off.`);
|
||||
|
|
@ -177,7 +175,7 @@ async function qaTriage(findings, counts) {
|
|||
const lines = top.map((f) => `- [${f.severity}] ${f.check_type} ${f.url}: ${f.detail}`).join("\n") || "(no issues found)";
|
||||
const fallback = `${counts.error} error(s) and ${counts.warning} warning(s) across ${counts.pages} pages.`;
|
||||
const ask = [
|
||||
"You are the QA agent for carlosarias.co. A crawler just tested the live site. Consult your `qa` skill.",
|
||||
`You are the QA agent for ${site.host}. A crawler just tested the live site. Consult your qa skill.`,
|
||||
`Counts: ${counts.error} errors, ${counts.warning} warnings across ${counts.pages} pages.`,
|
||||
"Top findings:", lines,
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
* reverts itself. Owns: /tasks/run. Exposes host.drainTasks for QA autofix.
|
||||
*/
|
||||
|
||||
import { site } from "../site.mjs";
|
||||
|
||||
let H;
|
||||
let draining = false;
|
||||
|
||||
|
|
@ -39,18 +41,18 @@ function buildDesignPrompt(task) {
|
|||
|
||||
const p = [];
|
||||
p.push(
|
||||
"You are the Web Designer for the Carlos Arias website (carlosarias.co) — an Astro + Tailwind v4 static site.",
|
||||
`You are the Web Designer for the ${site.name} website (${site.host}) — an Astro + Tailwind v4 static site.`,
|
||||
"You are a real designer: make considered, on-brand design decisions, not just literal edits.",
|
||||
"",
|
||||
"Before you start, consult your design skills and APPLY them: read .claude/skills/brand/SKILL.md and .claude/skills/ui-ux/SKILL.md.",
|
||||
"The brand is sumi-e — ink on washi paper, a single vermillion seal, restraint, near-square 2px radii. Reuse the existing .ca-* classes and --ca-* tokens in app/src/styles.css and match nearby components; do not invent new one-off styles or add a second accent colour.",
|
||||
"Before you start, consult your design skills and APPLY them: read .claude/skills/brand/SKILL.md and .claude/skills/ui-ux/SKILL.md, plus the brand guide in brand/BRAND.md.",
|
||||
"Reuse the existing utility classes and CSS tokens in app/src/styles.css and match nearby components; do not invent new one-off styles or add a second accent colour.",
|
||||
"Do NOT run builds or git — the console builds and publishes for you.",
|
||||
"",
|
||||
);
|
||||
if (isNew) {
|
||||
p.push(
|
||||
`TASK: create a NEW page at /${slug}.`,
|
||||
`- Create app/src/pages/${slug}.astro using BaseLayout and existing .ca-* section patterns (study app/src/pages/about.astro and services.astro for structure).`,
|
||||
`- Create app/src/pages/${slug}.astro using BaseLayout and the existing section patterns (study an existing page under app/src/pages, e.g. about.astro, for structure).`,
|
||||
"- Register it in the nav: add it to the nav array in app/src/components/Header.astro AND the footer links in app/src/components/Footer.astro.",
|
||||
"",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|||
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync } from "node:fs";
|
||||
import { dirname, resolve, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { site } from "./site.mjs";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url)); // agents/console
|
||||
const REPO = resolve(HERE, "..", ".."); // repo root
|
||||
|
|
@ -25,9 +26,9 @@ const ENV_FILE = join(REPO, "agents", ".env");
|
|||
const CLAUDE = "/usr/local/bin/claude";
|
||||
const PHP = "/usr/bin/php";
|
||||
const PORT = Number(process.env.ADMIN_PORT || 3011);
|
||||
const AGENT_TOOLS = "Read Write Edit Glob Grep WebSearch";
|
||||
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
||||
const GIT_SCOPE = ["app", "brand", "api/db", "api/cli"]; // paths the console may commit
|
||||
const AGENT_TOOLS = site.tools; // from astroagent.config.json ai.tools
|
||||
const DEFAULT_MODEL = site.model; // from astroagent.config.json ai.model
|
||||
const GIT_SCOPE = ["app", "brand", "api/db", "api/cli"]; // SeedProject-conventional commit scope
|
||||
|
||||
// ---- env (.env) --------------------------------------------------------------
|
||||
function loadEnv() {
|
||||
|
|
@ -53,7 +54,7 @@ function agentEnv() {
|
|||
const env = loadEnv();
|
||||
return {
|
||||
...process.env,
|
||||
HOME: process.env.HOME || "/var/lib/carlos-arias-agent",
|
||||
HOME: process.env.HOME || site.home,
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin",
|
||||
CLAUDE_CODE_OAUTH_TOKEN: env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
||||
};
|
||||
|
|
|
|||
43
agents/console/site.mjs
Normal file
43
agents/console/site.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Site identity for the AstroAgent runtime — the ONLY place the framework reads
|
||||
* who this deployment is. Everything site-specific (name, url, model, agent user,
|
||||
* QA target) is derived here from site.config.json + astroagent.config.json, so
|
||||
* host.mjs and the agent modules stay site-agnostic. Brand *rules* live in the
|
||||
* per-site brand guide (brand/BRAND.md) and the `brand` skill, not here.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const readJson = (p) => { try { return JSON.parse(readFileSync(join(REPO, p), "utf8")); } catch { return {}; } };
|
||||
|
||||
const sc = readJson("site.config.json");
|
||||
const aa = readJson("astroagent.config.json");
|
||||
|
||||
const url = String(sc.url || aa.url || "https://example.com").replace(/\/+$/, "");
|
||||
let host = url; try { host = new URL(url).host; } catch {}
|
||||
const agentUser = aa.ai?.agentUser || "astroagent";
|
||||
|
||||
export const site = {
|
||||
name: sc.name || "the site",
|
||||
url,
|
||||
host,
|
||||
model: aa.ai?.model || "claude-sonnet-4-6",
|
||||
tools: aa.ai?.tools || "Read Write Edit Glob Grep WebSearch",
|
||||
agentUser,
|
||||
home: `/var/lib/${agentUser}`,
|
||||
author: {
|
||||
name: sc.author?.name || sc.name || "the author",
|
||||
role: sc.author?.jobTitle || "",
|
||||
},
|
||||
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: [
|
||||
"/", "/about", "/services", "/services/website-design",
|
||||
"/projects", "/blog", "/contact", "/changelog", "/resume", "/faq",
|
||||
],
|
||||
};
|
||||
Loading…
Reference in a new issue