From 6fec3498f2452cb34407d31566e7e9ddb000650a Mon Sep 17 00:00:00 2001 From: Carlos Arias Date: Fri, 24 Jul 2026 08:05:38 -0400 Subject: [PATCH] framework(core): clone tooling + docs (Phase 4) - FRAMEWORK.md: the host + pluggable-agent contract, the roster, and the clone+assign flow - configure.mjs: ensure a default agents roster on a fresh clone - .memory/{qa-agent,webdesigner}.md: framework notes pointing at the module layout Verified: a scratch clone with a different site.config.json yields a different identity + roster from identical core code. --- .memory/qa-agent.md | 8 +++- .memory/webdesigner.md | 6 +++ agents/console/FRAMEWORK.md | 82 +++++++++++++++++++++++++++++++++++++ scripts/configure.mjs | 8 ++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 agents/console/FRAMEWORK.md diff --git a/.memory/qa-agent.md b/.memory/qa-agent.md index 60614a9..813a841 100644 --- a/.memory/qa-agent.md +++ b/.memory/qa-agent.md @@ -1,7 +1,13 @@ # The QA agent — automated site testing with a self-healing loop **Status:** Implemented · **Date:** 2026-07-24 -**Related:** [webdesigner.md](webdesigner.md) · [../.claude/skills/qa/SKILL.md](../.claude/skills/qa/SKILL.md) · [../agents/console/SETUP.md](../agents/console/SETUP.md) · [../agents/console/PLAN.md](../agents/console/PLAN.md) +**Related:** [webdesigner.md](webdesigner.md) · [../agents/console/FRAMEWORK.md](../agents/console/FRAMEWORK.md) · [../.claude/skills/qa/SKILL.md](../.claude/skills/qa/SKILL.md) · [../agents/console/SETUP.md](../agents/console/SETUP.md) + +> **Framework note (2026-07-24):** the console runner was refactored into a generic host + +> pluggable agent modules. QA now lives in `agents/console/agents/qa.mjs` (exports `manifest` +> + `register(host, settings)`), is enabled via the `astroagent.config.json` roster, and reads +> `heartbeatMin`/`autofix` from `agents.settings.qa`. Its logic is unchanged from below; see +> [FRAMEWORK.md](../agents/console/FRAMEWORK.md). --- diff --git a/.memory/webdesigner.md b/.memory/webdesigner.md index 32c1b0a..c30c0fe 100644 --- a/.memory/webdesigner.md +++ b/.memory/webdesigner.md @@ -28,6 +28,12 @@ It coexists with two sibling surfaces built earlier in the same arc: The Web Designer is the *freeform, queued* path for everything else. +> **Framework note (2026-07-24):** the console runner was refactored into a generic host + +> pluggable agent modules ([FRAMEWORK.md](../agents/console/FRAMEWORK.md)). The Web Designer now +> lives in `agents/console/agents/web-designer.mjs` (exports `manifest` + `register`), is enabled +> via the `astroagent.config.json` roster, and exposes `host.drainTasks` (QA autofix kicks it). +> Logic unchanged from below. + ## Added after first write (2026-07-24) Two capabilities were added to the Web Designer after this doc's first version: diff --git a/agents/console/FRAMEWORK.md b/agents/console/FRAMEWORK.md new file mode 100644 index 0000000..f2463d5 --- /dev/null +++ b/agents/console/FRAMEWORK.md @@ -0,0 +1,82 @@ +# AstroAgent — the standalone agent framework + +A generic **host** that runs **pluggable agents**, cloneable into any SeedProject site, where +each project **enables the agents it needs** via config. The host knows nothing site-specific; +identity comes from config, brand from the per-site `brand` skill. + +## Module layout (`agents/console/`) + +| File | Role | +|---|---| +| `host.mjs` | The generic runtime — **site-agnostic**. Shared primitives + route registry + scheduler + core routes (`/ping`, `/auth`, `/logout`). | +| `site.mjs` | The **only** place identity is read. Derives `name / url / host / model / agentUser / home / role / qaEmail / qaRoutes / agents` from `site.config.json` + `astroagent.config.json`. | +| `server.mjs` | Bootstrap: load host → register the **enabled** agents (with their settings) → `host.listen()`. | +| `agents/.mjs` | One agent each — `in-page-console`, `project-builder`, `web-designer`, `qa`. Site-agnostic; identity via `site.*`, brand via `brand/BRAND.md` + the `brand` skill. | + +## Agent module contract + +Each `agents/.mjs` exports a `manifest` and a `register`: + +```js +export const manifest = { + name: "qa", title: "QA Agent", class: "runtime", + description: "…", + triggers: [ { type:"endpoint", method:"POST", path:"/qa/run" }, + { type:"schedule", settingKey:"heartbeatMin" } ], + tools: "Read Skill", skills: ["qa"], + tables: ["cja_qa_runs","cja_qa_findings"], // migrations this agent needs + cli: ["qa-start","qa-finish","qa-routes","qa-autofix"], // api/cli/*.php bridges it needs +}; + +export function register(host, settings = {}) { + host.route("POST", "/qa/run", (req, res) => { /* uses host.* */ }); + if ((settings.heartbeatMin ?? 60) > 0) host.everyMinutes(settings.heartbeatMin, fn); +} +``` + +`register` receives the host + this agent's per-project `settings`. It wires routes/triggers +and uses **only** `host.*` primitives — never globals — so an agent is a self-contained unit. + +## Host API (`host.*`) + +- `route(method, path, handler)` — register an HTTP route (`method:"ANY"` matches any verb). +- `runClaudeJson({prompt, tools, model})` → `{ok, result}` — one-shot Claude. +- `runClaudeStream({prompt, tools, model, resume, onMessage, …})` — streaming Claude (SSE). +- `build(extraEnv)`, `git(args)`, `phpCli(args)` — build / git / the DB bridge. +- `everyMinutes(min, fn, {bootDelayMs})` — scheduled trigger (heartbeat). +- `newJob/emit/jobs` — SSE job model. `readBody/json` — HTTP helpers. +- `state.busy` — the shared single-flight lock (mutable across agent modules). +- `gitScope`, `REPO`, `APP`, `PREVIEW_DIR`, `DEFAULT_MODEL`, `AGENT_TOOLS` — framework constants. +- Extension points agents attach, e.g. `host.drainTasks` (web-designer) which QA autofix calls. + +## The roster — assign agents to a project + +`astroagent.config.json`: +```json +"agents": { + "enabled": ["in-page-console", "project-builder", "web-designer", "qa"], + "settings": { "qa": { "heartbeatMin": 60, "autofix": true } } +} +``` +`server.mjs` loads only `enabled` agents. Disable one → its routes 404 and its triggers stop. +A restaurant site might enable `["web-designer","qa","menu-updater"]`; a law firm +`["web-designer","qa","seo","intake-triage"]` — same framework, different roster. + +## Clone + assign + +1. Clone the base → `scripts/new-site.sh` (or edit `site.config.json` + `node scripts/configure.mjs`) — stamps identity (name, url, agentUser, timezone…). `site.mjs` picks these up automatically. +2. Set `astroagent.config.json → agents.enabled` to the roster this project needs; tune `settings`. +3. Provision each enabled agent's dependencies — its `tables` (migrations), its `cli` (`api/cli/*.php`), its `skills` (`.claude/skills/*` or MCP). The manifest lists them; **`node agents/catalog.mjs` → `agents/agents.json`, diff it against a clone to find gaps.** +4. Start the console service → the host loads exactly the enabled agents. + +## Add a new agent + +Drop `agents/console/agents/.mjs` exporting `manifest` + `register`, add its name to +`agents.enabled`, provide its migrations/CLI/skills, run `node agents/catalog.mjs`, restart. + +## What stays site-specific (never in the core) + +`site.config.json` + `astroagent.config.json` (identity + roster), the `brand` skill + +`brand/BRAND.md` (visual rules), the `cja_*` content, and the built site. The registry +(`agents/agents.json`, generated) unifies pipeline (`agents/scripts/*`) and runtime (console) +agents so one manifest diff covers a whole clone. diff --git a/scripts/configure.mjs b/scripts/configure.mjs index 38fb4f1..f51da5c 100755 --- a/scripts/configure.mjs +++ b/scripts/configure.mjs @@ -57,6 +57,14 @@ if (existsSync(resolve(root, "astroagent.config.json"))) { aa.name = slug(site.name); aa.url = site.url; if (aa.ai) aa.ai.agentUser = `${slug(site.name)}-agent`; + // Ensure a console agent roster exists (which agents this clone runs). The + // enabled list + settings are the operator's to edit; only default if absent. + if (!aa.agents) { + aa.agents = { + enabled: ["in-page-console", "project-builder", "web-designer", "qa"], + settings: { qa: { heartbeatMin: 60, autofix: true } }, + }; + } write("astroagent.config.json", aa); }